I'm working on an audio editing app, and I need to store the samples as floats in an efficient way - memory as well as performance-wise. Right now I'm using simply Kotlin's List, but I've heard about potential gains from using FloatArray type. I made a synthetic test to see the benefits and the results are a bit weird. Any advice on the techniques and collections I should use for large datasets of this nature could earn you a cookie, if I had one and knew your location.
So I have two alternative channel implementations (as in - channel in an audio file), to hold my data.
Here is my code for the channel based on FloatArray:
class ArrayChannel {
private var mData : FloatArray = FloatArray(0)
private var mLastWrittenIndex = 0
fun getSamples(startIndex : Int = 0, endIndex : Int = mLastWrittenIndex) : FloatArray
= mData.sliceArray(IntRange(0, endIndex - 1))
fun insertSamples(samples : FloatArray, startIndex : Int = mLastWrittenIndex) {
if (mData.size - mLastWrittenIndex < samples.size) {
val newData = FloatArray(mData.size + samples.size )
mData.copyInto(newData, 0, 0, startIndex)
samples.copyInto(newData, startIndex)
mData.copyInto(newData, startIndex + samples.size, startIndex)
mData = newData
} else {
mData.copyInto(mData, startIndex + samples.size, startIndex, mLastWrittenIndex)
samples.copyInto(mData, startIndex)
}
mLastWrittenIndex += samples.size
}
fun getSamplesSize(): Int = mLastWrittenIndex
}
And here is my code for the channel based on List:
class Channel {
private var mData = mutableListOf<Float>()
fun getSamples(startIndex : Int = 0, endIndex : Int = mData.size) : List<Float>
= mData.slice(IntRange(0, endIndex - 1))
fun insertSamples(samples : List<Float>, startIndex : Int = mData.size) {
mData.addAll(startIndex, samples)
}
fun getSamplesSize() : Int = mData.size
}
Here is the measuring code:
val initialValuesArray = FloatArray(1000000) {Random.nextFloat()}
val valuesToAddArray = FloatArray(1000000) {Random.nextFloat()}
val initialValuesList = MutableList(1000000) {Random.nextFloat()}
val valuesToAddList = MutableList(1000000) {Random.nextFloat()}
var startTime = System.currentTimeMillis()
val arrayChannel = ArrayChannel()
arrayChannel.insertSamples(initialValuesArray)
arrayChannel.insertSamples(valuesToAddArray, 0)
println("Array time: ${System.currentTimeMillis() - startTime}")
startTime = System.currentTimeMillis()
val listChannel = Channel()
listChannel.insertSamples(initialValuesList)
listChannel.insertSamples(valuesToAddList, 0)
println("List time: ${System.currentTimeMillis() - startTime}")
Now, the average results from a direct fun main() call in Android studio are as following:
Array time: 56
List time: 6
A change in the allocation of the array, instead of mData.size + samples.size to mData.size + samples.size * 2, makes these different, in a very weird way:
Array time: 17
List time: 48
When I run the same code inside of an Activity instead of some main Kotlin function, the results match more what I was expecting and are promising:
2020-08-17 21:15:33.325 D/ARRAY_TIME: 15
2020-08-17 21:15:33.481 D/LIST_TIME: 156
Why the code behaves this way and what do you think would be a good way of handling lots of numerical data in the Android environment?
Related
I made a stat calculator and I need to store 10 user inputted values to be stored in an array where I can get the average and lowest number. How do I go about getting the values on the array to get the maximum and minimum values?
var values = ArrayList<Int>()
var count = 0
var arrCount = 0
addButt.setOnClickListener {
values.add(statNum.text.toString().toInt())
count++
arrCount++
var i = 0
statNum.setText("")
statArray.text = ""
for (i in 0..arrCount - 1) {
statArray.text = statArray.text as String + values[i] + ","
}
}
avgBut.setOnClickListener{
val statArray = doubleArrayOf(4.0, 7.0, 11.0, 12.0, 9.0)
var sum = 0.0
for (num in statArray) {
sum += num
}
val average = sum / statArray.size
finalAnswer.text = average.toString()
}
minmaxBut.setOnClickListener{
fun findMin(list: List<Int?>): Int? {
return list.sortedWith(compareBy { it }).first()
}
fun findMax(list: List<Int?>): Int? {
return list.sortedWith(compareBy { it }).last()
}
fun main() {
val list = listOf(10, 4, 2, 7, 6, 9)
val min = findMin(list)
finalAnswer.text = "Min Vale = $min"
val max = findMax(list)
println(max) // 10
}
It's very easy to get min, max and average number in the arraylist. I did not understand your code clearly but i will share how you can do these. After you may use that on your array:
val list = arrayListOf<Int>(1,2,3,4,5,6,7,8,9,10)
Log.e("min", list.min().toString())
Log.e("max", list.max().toString())
Log.e("average", list.average().toString())
This will give you below output in logcat:
E/min: 1
E/max: 10
E/average: 5.5
For some reason I can't understand, when I'm parsing the buffer values, I almost never get the version as 4 (which would be the default for IPv4), and most of the time I get some random value like 10 or 0 instead. The same is true with the protocol, even though it is parsing exactly as it is specified on the wiki, it never comes up precisely with something that says it's TCP or UDP, just 0 (which would map to HOPOPT) or a random value. I believe the parsing is at least correct because when I debug, the source address resolves to the DNS I specified (8.8.4.4), which shouldn't happen if I was parsing the wrong way (at least, it seems to me that no).
val (version, internetHeaderLength) = buffer.get().toUByte().split()
// Differentiated Services Code Point, Explicit Congestion Notification (TOS)
val (dscp, ecn) = buffer.get().toUByte().extract(2)
val totalLength = buffer.short.toUShort()
val identification = buffer.short.toUShort()
val (flags, fragmentOffset) = buffer.short.toUShort().extract(3)
val timeToLive = buffer.get().toUByte()
val protocol = buffer.get().toUByte()
val headerChecksum = buffer.short.toUShort()
val sourceAddress = buffer.int
val destinationAddress = buffer.int
val options = buffer.array().sliceArray(0 until internetHeaderLength.toInt() * 4 - 20)
val data =
buffer.array().sliceArray(internetHeaderLength.toInt() * 4 until totalLength.toInt())
Below is the code I use to get the packet data
fun receivePackets(input: FileInputStream, output: FileOutputStream, buffer: ByteBuffer) {
val read = input.read(buffer.array())
if (read > 0) {
buffer.limit(read)
val packet = IpDatagram(buffer)
Log.d(Tag, "Received packet: $packet which is " +
if (packet.valid) "valid" else "invalid"
)
(the code I wrote for handling numbers bits)
fun UShort.getBit(position: Int): Int {
return (this.toInt() shr position) and 1;
}
fun UShort.setBit(position: Int, value: Int): UShort {
return (this.toInt() or (value shl position)).toUShort();
}
fun UShort.setBit(position: Int, value: Boolean): UShort {
return (this.toInt() or ((if (value) 1 else 0) shl position)).toUShort();
}
fun UShort.split(): Pair<UShort, UShort> {
return Pair((this.toInt() shr 4).toUShort(), (this.toInt() and 0x0F).toUShort());
}
fun UShort.extract(count: Int): Pair<UShort, UShort> {
return Pair(this.extractFirst(count), this.extractLast(count))
}
fun UShort.extractFirst(count: Int): UShort {
return (this.toInt() shr count).toUShort();
}
fun UShort.extractLast(count: Int): UShort {
return (this.toInt() and ((1 shl count) - 1)).toUShort();
}
I've been debugging this for a while, and I've already tried some things like decomposing the numbers that result to try to see if there are any errors in the methods I created to decompose the integers or if they present patterns like 0100 that I want for IPv4 identification.
Example of one packet after being parsed:
{
"destinationAddress":"95.53.97.33",
"dscp":9,
"ecn":0,
"flags":0,
"fragmentOffset":0,
"headerChecksum":0,
"identification":-384,
"internetHeaderLength":0,
"options":[],
"payload":[96],
"protocol":"HOPOPT" // 0,
"sourceAddress":"101.94.226.95",
"timeToLive":0,
"totalLength":1,
"valid":true,
"version":0
}
I am currently attempting to do something very similar (or maybe the same) as the following question:
Getting variable frequency ranges with androids visualizer class
However, the selected answer has a few bugs, and I'm not a DSP/Audio expert at all and I'm learning as I go.
My goal is to break an FFT that I'm getting from Android Visualizer class into frequency bands. Specifically, these bands:
0Hz - 400Hz
400Hz - 900Hz
900Hz - 1500Hz
1500Hz - 2300Hz
2300Hz - 3400Hz
3400Hz - 5000Hz
5000Hz - 7300Hz
7300Hz - 12000Hz
I have the following code, at the top of my class:
private val targetEndpoints = listOf(0f, 400f, 900f, 1500f, 2300f, 3400f, 5000f, 7300f, 12000f)
private const val CAPTURE_SIZE = 1024
and then, in the method where I'm trying to get the frequency bands for the current track in MediaPlayer:
val mp = mediaPlayer!!
val audioSessionId = mp.getAudioSessionId()
val visualizer: Visualizer = Visualizer(audioSessionId)
val captureSizeRange = Visualizer.getCaptureSizeRange().let { it[0]..it[1] }
val captureSize = CAPTURE_SIZE.coerceIn(captureSizeRange)
val captureRate: Int = Visualizer.getMaxCaptureRate()
val isWaveFormRequested: Boolean = false
val isFFTRequested: Boolean = true
visualizer.setCaptureSize(captureSize)
val frequencyOrdinalRanges: List<IntProgression> =
targetEndpoints.zipWithNext { a, b ->
val startOrdinal = 1 + (captureSize * a / samplingRate).toInt()
val endOrdinal = (captureSize * b / samplingRate).toInt()
startOrdinal downTo endOrdinal
}
Now this is the point where things are getting a little murky for me because, like I said, I am no Audio expert.
frequencyOrdinalRanges is a List with IntProgressions that go 1 -> 0
For the audio file that I'm using:
captureSize = 1024
samplingRate = 44100000
With those numbers and my frequency bands, pretty much guarantees that the startOrdinal will always be 1, and endOrdinal will always be 0.
So my frequencyOrdinalRanges looks like this:
[1 downTo 0 step 1, 1 downTo 0 step 1, 1 downTo 0 step 1, 1 downTo 0 step 1, 1 downTo 0 step 1, 1 downTo 0 step 1, 1 downTo 0 step 1]
Then I've got an Listener with a capture rate of 20000 milihertz:
visualizer.setDataCaptureListener(listener, captureRate, isWaveFormRequested, isFFTRequested)
The values for the above call are as follows:
captureRate = 2000 // in milihertz
isWaveFormRequested = false
isFFTRequested = true
The onFftDataCapture of the listener object looks as follows:
override fun onFftDataCapture(visualizer: Visualizer, bytes: ByteArray, samplingRate: Int) {
var output = DoubleArray(frequencyOrdinalRanges.size)
for ((i, ordinalRange) in frequencyOrdinalRanges.withIndex()) {
var logMagnitudeSum = 0.0
for (k in ordinalRange) {
val fftIndex = k * 2
val currentByte = bytes[fftIndex].toDouble()
val nextByte = bytes[fftIndex + 1].toDouble()
val hypot = Math.hypot(currentByte, nextByte)
val logHypot = Math.log10(hypot)
logMagnitudeSum += logHypot
val result = (logMagnitudeSum / (ordinalRange.last - ordinalRange.first + 1)).toDouble()
output[i] = result
}
// do something else with output
}
Now the problem I'm facing with onFftDataCapture is that this line:
val hypot = Math.hypot(currentByte, nextByte)
it often evaluates to 0, thus making the following line evaluate to -Infinity and ultimately giving me an array full of Infinity values which I can't do anything with.
This leads me to believe that I am doing something very wrong, but I am not sure what or how to fix it.
This answer looks more or less what I am trying to do, but then again, I am no expert in audio analysis, so all the finer details totally escape me.
The way to extract 10-band equalization information from mp3 format
Can someone tell me what am I doing wrong? or what am I missing?
The problem with my code was quite silly... I was using samplingRate in milihertz... the formula expects the sampling rate to be in Hertz.
Dividing samplingRate by 1000 fixed the problems.
This changes to:
val samplingRateInHz = samplingRate / 1000
val frequencyOrdinalRanges: List<IntRange> =
targetEndpoints.zipWithNext { a, b ->
val startOrdinal = 1 + (captureSize * a / samplingRateInHz).toInt()
val endOrdinal = (captureSize * b / samplingRateInHz).toInt()
startOrdinal..endOrdinal
}
I have a codebase where all my data calculations and data formatting occur within a single function. Are there any performance benefits to separating these out into separate functions or does separating them only improve readability?
I know that I should separate them but I don't really know all the reasons why.
Here is the function I am referring to:
private fun processData(data: ByteArray) {
progressBar.visibility = GONE
Log.d(TAG, "displayDiagnosticData: ")
val bmsVersionView = findViewById<TextView>(R.id.textview_bms_version)
val boardVersionView = findViewById<TextView>(R.id.textview_board_version)
val cellOneView = findViewById<TextView>(R.id.textview_cell_1)
val cellTwoView = findViewById<TextView>(R.id.textview_cell_2)
val cellThreeView = findViewById<TextView>(R.id.textview_cell_3)
val cellFourView = findViewById<TextView>(R.id.textview_cell_4)
val cellFiveView = findViewById<TextView>(R.id.textview_cell_5)
val cellSixView = findViewById<TextView>(R.id.textview_cell_6)
val cellSevenView = findViewById<TextView>(R.id.textview_cell_7)
val cellEightView = findViewById<TextView>(R.id.textview_cell_8)
val cellNineView = findViewById<TextView>(R.id.textview_cell_9)
val cellTenView = findViewById<TextView>(R.id.textview_cell_10)
val cellElevenView = findViewById<TextView>(R.id.textview_cell_11)
val cellTwelveView = findViewById<TextView>(R.id.textview_cell_12)
val cellThirteenView = findViewById<TextView>(R.id.textview_cell_13)
val cellFourteenView = findViewById<TextView>(R.id.textview_cell_14)
val packTotalView = findViewById<TextView>(R.id.textview_diagnostic_voltage)
val packSocView = findViewById<TextView>(R.id.textview_diagnostic_soc)
val chargeTempView = findViewById<TextView>(R.id.textview_charge_temp)
val dischargeTempView = findViewById<TextView>(R.id.textview_discharge_temp)
val chargeCurrentView = findViewById<TextView>(R.id.textview_diagnostic_charge_current)
// val dischargeCurrentView = findViewById<TextView>(R.id.textview_diagnostic_discharge_current)
val dischargeCircuitStateView = findViewById<TextView>(R.id.textview_discharge_circuit)
val chargeCircuitStateView = findViewById<TextView>(R.id.textview_charge_circuit)
val balanceCircuitStateView = findViewById<TextView>(R.id.textview_balance_circuit)
val emptyCircuitStateView = findViewById<TextView>(R.id.textview_empty_circuit)
val bmsVersion = data[0] + (data[1] * 256)
val cellOne = data[2].toDouble() / 100 + 3.52
val cellTwo = data[3].toDouble() / 100 + 3.52
val cellThree = data[4].toDouble() / 100 + 3.52
val cellFour = data[5].toDouble() / 100 + 3.52
val cellFive = data[6].toDouble() / 100 + 3.52
val cellSix = data[7].toDouble() / 100 + 3.52
val cellSeven = data[8].toDouble() / 100 + 3.52
val cellEight = data[9].toDouble() / 100 + 3.52
val cellNine = data[10].toDouble() / 100 + 3.52
val cellTen = data[11].toDouble() / 100 + 3.52
val cellEleven = data[12].toDouble() / 100 + 3.52
val cellTwelve = data[13].toDouble() / 100 + 3.52
val cellThirteen = data[14].toDouble() / 100 + 3.52
val cellFourteen = data[15].toDouble() / 100 + 3.52
val totalVoltage = 47.8 + (data[16].toDouble() / 10)
val chargeTempCelsius = data[19]
val dischargeTempCelsius = data[20]
val chargeTempFahr = (chargeTempCelsius * 9.0 / 5.0) + 32.0
val dischargeTempFahr = (dischargeTempCelsius * 9.0 / 5.0) + 32.0
val chargeCurrent = data[21]
// val dischargeCurrent = (data[23].toDouble() * 100 + data[22]).toInt()
val chargeCircuitState = data[25].toInt()
val dischargeCircuitState = data[26].toInt()
val balanceCircuitState = data[27].toInt()
val emptyCircuitState = data[28].toInt()
val chargeCircuit: String = if (chargeCircuitState == 1) {
"On"
}else {
"Off"
}
val dischargeCircuit: String = if (dischargeCircuitState == 1) {
"On"
}else {
"Off"
}
val balanceCircuit: String = if (balanceCircuitState == 1) {
"On"
}else {
"Off"
}
val emptyCircuit: String = if (emptyCircuitState == 1) {
"On"
}else {
"Off"
}
val bmsVersionString = SpannableString("BMS Version: $bmsVersion")
bmsVersionString.setSpan(StyleSpan(Typeface.BOLD), 0, 11, 0)
val boardVersionString = SpannableString("Board Version: 2.1")
boardVersionString.setSpan(StyleSpan(Typeface.BOLD), 0, 13, 0)
val cellOneString = SpannableString("Cell 1: %.2fV".format(cellOne))
cellOneString.setSpan(StyleSpan(Typeface.BOLD), 0, 6, 0)
val cellTwoString = SpannableString("Cell 2: %.2fV".format(cellTwo))
cellTwoString.setSpan(StyleSpan(Typeface.BOLD), 0, 6, 0)
val cellThreeString = SpannableString("Cell 3: %.2fV".format(cellThree))
cellThreeString.setSpan(StyleSpan(Typeface.BOLD), 0, 6, 0)
val cellFourString = SpannableString("Cell 4: %.2fV".format(cellFour))
cellFourString.setSpan(StyleSpan(Typeface.BOLD), 0, 6, 0)
val cellFiveString = SpannableString("Cell 5: %.2fV".format(cellFive))
cellFiveString.setSpan(StyleSpan(Typeface.BOLD), 0, 6, 0)
val cellSixString = SpannableString("Cell 6: %.2fV".format(cellSix))
cellSixString.setSpan(StyleSpan(Typeface.BOLD), 0, 6, 0)
val cellSevenString = SpannableString("Cell 7: %.2fV".format(cellSeven))
cellSevenString.setSpan(StyleSpan(Typeface.BOLD), 0, 6, 0)
val cellEightString = SpannableString("Cell 8: %.2fV".format(cellEight))
cellEightString.setSpan(StyleSpan(Typeface.BOLD), 0, 6, 0)
val cellNineString = SpannableString("Cell 9: %.2fV".format(cellNine))
cellNineString.setSpan(StyleSpan(Typeface.BOLD), 0, 6, 0)
val cellTenString = SpannableString("Cell 10: %.2fV".format(cellTen))
cellTenString.setSpan(StyleSpan(Typeface.BOLD), 0, 7, 0)
val cellElevenString = SpannableString("Cell 11: %.2fV".format(cellEleven))
cellElevenString.setSpan(StyleSpan(Typeface.BOLD), 0, 7, 0)
val cellTwelveString = SpannableString("Cell 12: %.2fV".format(cellTwelve))
cellTwelveString.setSpan(StyleSpan(Typeface.BOLD), 0, 7, 0)
val cellThirteenString = SpannableString("Cell 13: %.2fV".format(cellThirteen))
cellThirteenString.setSpan(StyleSpan(Typeface.BOLD), 0, 7, 0)
val cellFourteenString = SpannableString("Cell 14: %.2fV".format(cellFourteen))
cellFourteenString.setSpan(StyleSpan(Typeface.BOLD), 0, 7, 0)
val packTotalString = SpannableString("Pack Total: %.1fV".format(totalVoltage))
packTotalString.setSpan(StyleSpan(Typeface.BOLD), 0, 10, 0)
val socString = SpannableString("SOC: ${data[17].toInt()}%")
socString.setSpan(StyleSpan(Typeface.BOLD), 0, 3, 0)
val chargeTempString = SpannableString("Charge Temp: ${chargeTempFahr.toInt()}" + "°F")
chargeTempString.setSpan(StyleSpan(Typeface.BOLD), 0, 11, 0)
val dischargeTempString = SpannableString("Discharge Temp: ${dischargeTempFahr.toInt()}" + "°F")
dischargeTempString.setSpan(StyleSpan(Typeface.BOLD), 0, 15, 0)
val chargeCurrentString = SpannableString("Charge Current: $chargeCurrent" + "A")
chargeCurrentString.setSpan(StyleSpan(Typeface.BOLD), 0, 14, 0)
// val dischargeCurrentString = "Discharge Current: $dischargeCurrent" + "A"
val chargeCircuitStateString = SpannableString("Charge Circuit: $chargeCircuit")
chargeCircuitStateString.setSpan(StyleSpan(Typeface.BOLD), 0, 14, 0)
val dischargeCircuitStateString = SpannableString("Discharge Circuit: $dischargeCircuit")
dischargeCircuitStateString.setSpan(StyleSpan(Typeface.BOLD), 0, 17, 0)
val balanceCircuitStateString = SpannableString("Balance Circuit: $balanceCircuit")
balanceCircuitStateString.setSpan(StyleSpan(Typeface.BOLD), 0, 15, 0)
val emptyCircuitStateString = SpannableString("Empty Circuit: $emptyCircuit")
emptyCircuitStateString.setSpan(StyleSpan(Typeface.BOLD), 0, 13, 0)
bmsVersionView.text = bmsVersionString
boardVersionView.text = boardVersionString
cellOneView.text = cellOneString
cellTwoView.text = cellTwoString
cellThreeView.text = cellThreeString
cellFourView.text = cellFourString
cellFiveView.text = cellFiveString
cellSixView.text = cellSixString
cellSevenView.text = cellSevenString
cellEightView.text = cellEightString
cellNineView.text = cellNineString
cellTenView.text = cellTenString
cellElevenView.text = cellElevenString
cellTwelveView.text = cellTwelveString
cellThirteenView.text = cellThirteenString
cellFourteenView.text = cellFourteenString
packTotalView.text = packTotalString
packSocView.text = socString
chargeTempView.text = chargeTempString
dischargeTempView.text = dischargeTempString
chargeCurrentView.text = chargeCurrentString
// dischargeCurrentView.text = dischargeCurrentString
chargeCircuitStateView.text = chargeCircuitStateString
dischargeCircuitStateView.text = dischargeCircuitStateString
balanceCircuitStateView.text = balanceCircuitStateString
emptyCircuitStateView.text = emptyCircuitStateString
}
Performance? No. Maintainability? Yes. Separation of concerns is one of the core tenets of modern software architecture. Combining the two things makes a difficult to read, difficult to debug mess. Separating them allows you to concentrate on one thing at a time, and makes it easier for people maintaining the code (which may even be you 6 months from now when fixing a bug and you've forgotten how it works) to understand the logic and flow of the program.
That function you posted would not be accepted in any professional codebase. It's too long, it does too many things. It needs to be broken up.
Your bigger problem there is that you're repeating yourself, a lot. Not only does that make the whole thing longer, and arguably harder to read, it also makes it hard to maintain like Gabe says, and makes it far more likely a bug will sneak in there. Imagine you needed to add another row of cells - there's a lot of boilerplate involved, a lot of repetitive work, and that's where humans tend to mess up
Like as an example of the kind of thing you can do - see how your cell data is basically taken from a range of values in data, with the same calculation applied to each? You could do this instead:
val cells = (2..15).map { index -> data[index].toDouble() / 100 + 3.52 }
or, to keep things more explicit and separate:
// Except give this a good name since it's doing something specific
// Because this is some kind of conversion formula, putting it in its own function
// makes it easy to maintain and document, and it's clear exactly what it is
fun getCellValue(dataValue: Int) = dataValue.toDouble() / 100 + 3.52
val cells = (2..15).map { index -> getCellValue(data[index]) }
Now you have one or two lines replacing 14 lines of initialisation code. It's also easier to make changes - if the format of data changes, you can easily change the range of indices to use, or the formula applied to each value. It's one place, instead of on each line, where you have to update each one and make sure you haven't made a typo or skipped one.
And when you have structured data like that, it can make your other code easier to work with too - because instead of needing to work with separate variables, you can work with indices and loop over things instead of writing each step out:
// no need for a separate line for each with hardcoded values if you can work it out
// (Because it's a separate function, you can use it for the other display lines too,
// it's not cell-specific)
fun SpannableString.applyBoldPrefix() = apply {
val colonIndex = indexOf(':')
if (colonIndex >= 0) setSpan(StyleSpan(Typeface.BOLD), 0, colonIndex, 0)
}
// you could also just pass in the index and look up cells[index] here -
// this is a more functional approach, but whatever's good
fun getCellDisplayString(cellIndex: Int, cellData: Double) =
SpannableString("Cell ${cellIndex + 1}: %.2fV".format(cellData))
.applyBoldPrefix()
// lots of ways to define/grab a set of views programmatically - putting them all
// in a container you can look up is one way. You can also generate resource identifier
// strings, like "R.id.textview_cell_$i" and look that up
val cellTextViews = findViewById<ViewGroup>(R.id.containerHoldingCells)
.children.filterIsInstance<TextView>
// now you can just iterate over stuff to complete the repetitive task
cellTextViews.forEachIndexed { i, view ->
view.text = getCellDisplayString(i, cells[i])
}
That's about half your code right there. I wouldn't necessarily structure everything this way (I feel like given you're working with a data format here, a more formal structure definition would be helpful, and you can generalise more too) and it's a bit rough-and-ready, but hopefully it gives you a general sense of how you can cut things down, but also make it easier to maintain them and try out changes
I condensed my code a lot more and created functions to process separate chunks of data. I also was able to get rid of a lot of my repetitious code. For anyone who is interested, here is the updated code:
private fun String.withStyling() = SpannableString(this).apply {
setSpan(StyleSpan(Typeface.BOLD), 0, indexOfFirst { it == ':' }, 0)
}
private fun processDiagnosticData(data: ByteArray) {
binding.progressBarCyclic.visibility = GONE
Log.d(TAG, "displayDiagnosticData: ")
processCells(data)
processTemps(data[19], data[20])
processCircuits(data)
processOtherData(data)
}
// Process cells 1-14 and display.
private fun processCells(data: ByteArray) {
val cellViews = listOf(
binding.textviewCell1,
binding.textviewCell2,
binding.textviewCell3,
binding.textviewCell4,
binding.textviewCell5,
binding.textviewCell6,
binding.textviewCell7,
binding.textviewCell8,
binding.textviewCell9,
binding.textviewCell10,
binding.textviewCell11,
binding.textviewCell12,
binding.textviewCell13,
binding.textviewCell14
)
for ((i, cellView) in cellViews.withIndex()) {
val value = data[2 + i].toDouble() / 100 + 3.52
val cellNumberString = (i + 1).toString()
val formattedString = "Cell $cellNumberString: %.2fV".format(value).withStyling()
cellView.text = formattedString
}
}
// Process charge/discharge temps and display.
private fun processTemps(chargeTempCel: Byte, dischargeTempCel: Byte) {
val chargeTempFahr = chargeTempCel * 9.0 / 5.0 + 32.0
val dischargeTempFahr = dischargeTempCel * 9.0 / 5.0 + 32.0
val chargeTempString = "Charge Temp: $chargeTempFahr°F".withStyling()
val dischargeTempString = "Discharge Temp: $dischargeTempFahr°F".withStyling()
binding.textviewChargeTemp.text = chargeTempString
binding.textviewDischargeTemp.text = dischargeTempString
}
// Process circuit states and display.
private fun processCircuits(data: ByteArray) {
val circuitViews = listOf(
binding.textviewChargeCircuit,
binding.textviewDischargeCircuit,
binding.textviewBalanceCircuit,
binding.textviewEmptyCircuit
)
val circuitNames = listOf(
"Charge Circuit: ",
"Discharge Circuit: ",
"Balance Circuit: ",
"Empty Circuit: "
)
for ((i, circuit) in circuitViews.withIndex()) {
val value = if (data[25 + i].toInt() == 1) {
"On"
} else {
"Off"
}
val formattedString = (circuitNames[i] + value).withStyling()
circuit.text = formattedString
}
}
// Process the rest of the data and display.
private fun processOtherData(data: ByteArray) {
val totalVoltage = 47.8 + (data[16].toDouble() / 10)
val packSoc = data[17].toInt()
val chargeCurrent = data[21]
// val dischargeCurrent = (data[23].toDouble() * 100 + data[22]).toInt()
val bmsVersionString = "BMS Version: ${data[0] + (data[1] * 256)}".withStyling()
val boardVersionString = "Board Version: 2.1".withStyling()
val totalVoltageString = "Pack Total: %.1fV".format(totalVoltage).withStyling()
val packSocString = "SOC: ${packSoc}%".withStyling()
val chargeCurrentString = "Charge Current: ${chargeCurrent}A".withStyling()
// val dischargeCurrent = "Discharge Current: $dischargeCurrentA".withStyling()
binding.textviewBmsVersion.text = bmsVersionString
binding.textviewBoardVersion.text = boardVersionString
binding.textviewDiagnosticVoltage.text = totalVoltageString
binding.textviewDiagnosticSoc.text = packSocString
binding.textviewDiagnosticChargeCurrent.text = chargeCurrentString
// binding.textviewDiagnosticDischargeCurrent.text = dischargeCurrentString
}
Info
I have an Item class file as follows:
class Item(var color:String, var numValue:Int, var drawableID:Int){
init {
color = this.color
numValue = this.numValue
drawableID = this.drawableID
}
}
In the main code I create an array which contains 104 objects by default attributes:
var myItemClassArray = Array(104) { Item("", -99, -99) }
Also I have pictures in my drawable folder and I have their IDs in an array which is drawablesIDs:Array<Int>, and it contains 53 elements.
Problem
I want to assign my Item attributes as in this picture: https://i.stack.imgur.com/wFVsn.png I can do it for a similar problem (which has 106 objects and 53 drawables) with the code given in below:
for (i in 0 until 106) {
if (i < 13) {
myItemClassList[i+2].color = "kirmizi"
myItemClassList[i+2].numValue = i+1
myItemClassList[i+2].drawableID = drawablesIDs[i+1]
} else if (i in 13..25) {
myItemClassList[i+2].color = "siyah"
myItemClassList[i+2].numValue = (i+1)-13
myItemClassList[i+2].drawableID = drawablesIDs[i+1]
} else if (i in 26..38) {
myItemClassList[i+2].color = "yesil"
myItemClassList[i+2].numValue = (i+1)-26
myItemClassList[i+2].drawableID = drawablesIDs[i+1]
} else if (i in 39..51) {
myItemClassList[i+2].color = "mavi"
myItemClassList[i+2].numValue = (i+1)-39
myItemClassList[i+2].drawableID = drawablesIDs[i+1]
} else if (i in 52..64) {
myItemClassList[i+2].color = "kirmizi"
myItemClassList[i+2].numValue = (i+1)-52
myItemClassList[i+2].drawableID = drawablesIDs[(i+1)-52]
} else if (i in 65..77) {
myItemClassList[i+2].color = "siyah"
myItemClassList[i+2].numValue = (i+1)-65
myItemClassList[i+2].drawableID = drawablesIDs[i+1-65+13]
} else if (i in 78..90) {
myItemClassList[i+2].color = "yesil"
myItemClassList[i+2].numValue = (i+1)-78
myItemClassList[i+2].drawableID = drawablesIDs[i+1-78+26]
} else if (i in 91..103) {
myItemClassList[i+2].color = "mavi"
myItemClassList[i+2].numValue = (i+1)-91
myItemClassList[i+2].drawableID = drawablesIDs[i+1-91+39]
} else {
myItemClassList[0].color = "sahte"
myItemClassList[0].drawableID = drawablesIDs[0]
myItemClassList[1].color = "sahte"
myItemClassList[1].drawableID = drawablesIDs[0]
}
}
Is there a cleaner way to do this?
One can use lambda expression to create an array. For example:
val test = Array(28){i-> examples[i]}
This works fine with one "i" parameter. But if I want to try something like this:
val test = Array(28){if(i<13)-> examples[i]}
it gives me an error because of it's syntax is wrong.
More Simple Question
Let's say we have an array which has numbers from 0 to 28 like this:
val testNumbers= Array(28){i->i}
Now I want to create an array which will contain numbers from 0 to 10 using lambda.
How do I this:
val player6 = Array(10){(it<10) -> testNumbers[it]} // gives an syntax error
From what I could gather from your picture, I've made these three assumptions:
The numValue is grouped in groups of 13 items
Each group receives a color in the order: kirmizi -> siyah -> yesil -> mavi, then it cycles again
The drawable IDs cycles every 52 items
Based on this, I came up with the following solution:
data class Item(var color: String = "", var numValue: Int = -99, var drawableId: Int = -99)
fun main() {
val colors = listOf("kirmizi", "siyah", "yesil", "mavi")
val drawableIDs = (0..52).toList() // This is just a stub. in your case it will be your drawable lists
val edgeCase = arrayListOf(Item("sahte", drawableId = drawableIDs[0]), Item("sahte", drawableId = drawableIDs[0]))
val pattern = (0 until 104)
.map { index -> Pair(index, index / 13) }
.map { (index, group) ->
Item(
color = colors[group % 4],
numValue = index+1,
drawableId = drawableIDs[(index % 52) + 1]
)
}
val total = pattern + edgeCase
total.forEach { println(it) }
}
You can play around with it on this kotlin playground.
Is there a cleaner way to do this?
From what I gather, you want to initialize only the first 13 values of a contiguous array with 28 spaces, leaving the rest with either their default values or null.
The reason why your code doesn't work is because the Array initializer expects you to return an object. the if block by itself is not an expression in kotlin, so it doesn't evaluate to a value, so you need to provide an else branch for it to work.
val examples = Array(28) { if (i < 13) examples[i] else defaultExample }
This is stated in the Kotlin documentation for control flow:
If you're using if as an expression rather than a statement (for example, returning its value or assigning it to a variable), the expression is required to have an else branch.
More simple question
In this case you could just use take:
// If you don't want to modify it
val player6 = testNumbers.take(10)
.toTypedArray() // Since take returns a List, you need to turn it back into an array
// If you want to modify the items
val player6 = testNumbers.take(10)
.map { value -> modifyNumber(value) }
.toTypedArray()
Tip: In kotlin if you declare your constructor parameter with val or var they are already attributes from your class and you don't need to initialize manually in the init block.
/*
* You don't need to do this:
* class Item(var color: String, var numValue: Int, var drawableId: Int) {
* init {
* this.color = color
* this.numValue = numValue
* this.drawableId = drawableId
* }
* }
*/
// Kotlin already does it for you
class Item(var color: String, var numValue: Int, var drawableId: Int)
fun main() {
val myitem = Item("blue", 20, 100)
println(myitem.color)
println(myitem.numValue)
println(myitem.drawableId)
}
Here is a possible solution :
fun getColor(i: Int) = when (i) {
in 0..1 -> "sahte"
in 2..13, in 52..64 -> "kirmizi"
in 65..77, in 13..25 -> "siyah"
in 26..38, in 78..90 -> "yesil"
in 39..51, in 91..103 -> "mavi"
else -> ""
}
fun getNumValue(i: Int) = when (i) {
in 0..1 -> -99
in 2..13 -> i - 1
in 13..25 -> (i - 1) - 13
in 26..38 -> (i - 1) - 26
in 39..51 -> (i - 1) - 39
in 52..64 -> (i - 1) - 52
in 65..77 -> (i - 1) - 65
in 78..90 -> (i - 1) - 78
in 91..103 -> (i - 1) - 91
else -> -99
}
fun getDrawableID(i: Int) = when (i) {
in 0..1 -> drawablesIDs[0]
in 2..13, in 13..25, in 26..38, in 39..51 -> drawablesIDs[i - 1]
in 52..64 -> drawablesIDs[(i - 1) - 52]
in 65..77 -> drawablesIDs[i - 1 - 65 + 13]
in 78..90 -> drawablesIDs[i - 1 - 78 + 26]
in 91..103 -> drawablesIDs[i - 1 - 91 + 39]
else -> -99
}
val myItemClassArray = Array(104) {
Item(getColor(it), getNumValue(it), getDrawableID(it))
}
Maybe there is some mistakes in the different ranges.
The main advantages are :
each mapping is testable independently
no mutability