I am writing an instrument tuner app (for now starting with Guitar). For pitch detection I'm using TarsosDSP. It does detect the pitch correctly, however it is quite shaky - for example, I'll hit the (correctly tuned) D string on my Guitar, it correctly recognizes it as a D, but after a short moment it cycles through a bunch of random notes very quickly. I'm not sure how to best solve this. Here is my code which is responsible for detecting the pitch:
val dispatcher: AudioDispatcher = AudioDispatcherFactory.fromDefaultMicrophone(44100, 4096, 3072)
val pdh = PitchDetectionHandler { res, _ ->
val pitchInHz: Float = res.pitch
runOnUiThread { processing.closestNote(pitchInHz)}
}
val pitchProcessor: AudioProcessor =
PitchProcessor(PitchProcessor.PitchEstimationAlgorithm.FFT_YIN,
44100F, 4096, pdh)
dispatcher.addAudioProcessor(pitchProcessor)
val audioThread = Thread(dispatcher, "Audio Thread")
audioThread.start()
I have then written a function which is supposed to detect the closest note to the current pitch. In addition I tried to get the results "less shaky" by also writing a function which is supposed to find the closest pitch in hz and then using that result for the closestNote function thinking that this way I may get less different results (even though it should be the same, and I also don't notice any difference). Here are the two functions:
...
private val allNotes = arrayOf("A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#")
private val concertPitch = 440
...
/** detects closest note in A = 440hz with with equal temperament formula:
* pitch(i) = pitch(0) * 2^(i/12)
* therefore formula to derive interval between two pitches:
* i = 12 * log2 * (pitch(i)/pitch(o))
*/
fun closestNote(pitchInHz: Float) {
(myCallback as MainActivity).noteSize() //adjusts the font size of note
if (pitchInHz != -1F) {
val roundHz = closestPitch(pitchInHz)
val i = (round(log2(roundHz / concertPitch) * 12)).toInt()
val closestNote = allNotes[(i % 12 + 12) % 12]
myCallback?.updateNote(closestNote) // updates note text
}
}
private fun closestPitch(pitchInHz: Float): Float {
val i = (round(log2(pitchInHz / concertPitch) * 12)).toInt()
val closestPitch = concertPitch * 2.toDouble().pow(i.toDouble() / 12)
return closestPitch.toFloat()
}
Any ideas how I can get more consistent results? Thanks!
Solved it myself: TarsosDSP calculates a probability with every note being played. I set my closestNote function to only update the text if the probability is > 0.91 (I found that value to offer "stability" in terms of text not changing after hitting a string and still correctly recognizing the note without hitting the string multiple times/too hard, also tested it with an unplugged, non hollow body electric Guitar)
Related
I am creating a pixel art editor for Android, and as for all pixel art editors, a paint bucket (fill tool) is a must need.
To do this, I did some research on flood fill algorithms online.
I stumbled across the following video which explained how to implement an iterative flood fill algorithm in your code. The code used in the video was JavaScript, but I was easily able to convert the code from the video to Kotlin:
https://www.youtube.com/watch?v=5Bochyn8MMI&t=72s&ab_channel=crayoncode
Here is an excerpt of the JavaScript code from the video:
Converted code:
Tools.FILL_TOOL -> {
val seedColor = instance.rectangles[rectTapped]?.color ?: Color.WHITE
val queue = LinkedList<XYPosition>()
queue.offer(MathExtensions.convertIndexToXYPosition(rectangleData.indexOf(rectTapped), instance.spanCount.toInt()))
val selectedColor = getSelectedColor()
while (queue.isNotEmpty() && seedColor != selectedColor) { // While the queue is not empty the code below will run
val current = queue.poll()
val color = instance.rectangles.toList()[convertXYDataToIndex(instance, current)].second?.color ?: Color.WHITE
if (color != seedColor) {
continue
}
instance.extraCanvas.apply {
instance.rectangles[rectangleData[convertXYDataToIndex(instance, current)]] = defaultRectPaint // Colors in pixel with defaultRectPaint
drawRect(rectangleData[convertXYDataToIndex(instance, current)], defaultRectPaint)
for (index in expandToNeighborsWithMap(instance, current)) {
val candidate = MathExtensions.convertIndexToXYPosition(index, instance.spanCount.toInt())
queue.offer(candidate)
}
}
}
}
Now, I want to address two major issues I'm having with the code of mine:
Performance
Flooding glitch (fixed by suggestion from person in the comments)
Performance
A flood fill needs to be very fast and shouldn't take less than a second, the problem is, say I have a canvas of size 50 x 50, and I decide to fill in the whole canvas, it can take up to 8 seconds or more.
Here is some data I've compiled for the time it's taken to fill in a whole canvas given the spanCount value:
spanCount
approx time taken in seconds to fill whole canvas
10
<1 seconds
20
~2 seconds
40
~6 seconds
60
~15 seconds
100
~115 seconds
The conclusion from the data is that the flood fill algorithm is unusually slow.
To find out why, I decided to test out which parts of the code are taking the most time to compile. I came to the conclusion that the expandToNeighbors function is taking the most time out of all the other tasks:
Here is an excerpt of the expandToNeighbors function:
fun expandToNeighbors(instance: MyCanvasView, from: XYPosition): List<Int> {
var asIndex1 = from.x
var asIndex2 = from.x
var asIndex3 = from.y
var asIndex4 = from.y
if (from.x > 1) {
asIndex1 = xyPositionData!!.indexOf(XYPosition(from.x - 1, from.y))
}
if (from.x < instance.spanCount) {
asIndex2 = xyPositionData!!.indexOf(XYPosition(from.x + 1, from.y))
}
if (from.y > 1) {
asIndex3 = xyPositionData!!.indexOf(XYPosition(from.x, from.y - 1))
}
if (from.y < instance.spanCount) {
asIndex4 = xyPositionData!!.indexOf(XYPosition(from.x, from.y + 1))
}
return listOf(asIndex1, asIndex2, asIndex3, asIndex4)
}
To understand the use of the expandToNeighbors function, I would recommend watching the video that I linked above.
(The if statements are there to make sure you won't get an IndexOutOfBoundsException if you try and expand from the edge of the canvas.)
This function will return the index of the north, south, west, and east pixels from the xyPositionData list which contains XYPosition objects.
(The black pixel is the from parameter.)
The xyPositionData list is initialized once in the convertXYDataToIndex function, here:
var xyPositionData: List<XYPosition>? = null
var rectangleData: List<RectF>? = null
fun convertXYDataToIndex(instance: MyCanvasView, from: XYPosition): Int {
if (rectangleData == null) {
rectangleData = instance.rectangles.keys.toList()
}
if (xyPositionData == null) {
xyPositionData = MathExtensions.convertListOfSizeNToListOfXYPosition(
rectangleData!!.size,
instance.spanCount.toInt()
)
}
return xyPositionData!!.indexOf(from)
}
So, the code works fine (kind of) but the expandToNeighbors function is very slow, and it is the main reason why the flood fill algorithm is taking a long time.
My colleague suggested that indexOf may be slowing everything down, and that I should probably switch to a Map-based implementation with a key being XYPosition and a value being Int representing the index, so I replaced it with the following:
fun expandToNeighborsWithMap(instance: MyCanvasView, from: XYPosition): List<Int> {
var asIndex1 = from.x
var asIndex2 = from.x
var asIndex3 = from.y
var asIndex4 = from.y
if (from.x > 1) {
asIndex1 = rectangleDataMap!![XYPosition(from.x - 1, from.y)]!!
}
if (from.x < instance.spanCount) {
asIndex2 = rectangleDataMap!![XYPosition(from.x + 1, from.y)]!!
}
if (from.y > 1) {
asIndex3 = rectangleDataMap!![XYPosition(from.x, from.y - 1)]!!
}
if (from.y < instance.spanCount) {
asIndex4 = rectangleDataMap!![XYPosition(from.x, from.y + 1)]!!
}
return listOf(asIndex1, asIndex2, asIndex3, asIndex4)
}
It functions the same way, only this time it uses a Map which is initialized here:
var xyPositionData: List<XYPosition>? = null
var rectangleData: List<RectF>? = null
var rectangleDataMap: Map<XYPosition, Int>? = null
fun convertXYDataToIndex(instance: MyCanvasView, from: XYPosition): Int {
if (rectangleData == null) {
rectangleData = instance.rectangles.keys.toList()
}
if (xyPositionData == null) {
xyPositionData = MathExtensions.convertListOfSizeNToListOfXYPosition(
rectangleData!!.size,
instance.spanCount.toInt()
)
}
if (rectangleDataMap == null) {
rectangleDataMap = MathExtensions.convertListToMap(
rectangleData!!.size,
instance.spanCount.toInt()
)
}
return xyPositionData!!.indexOf(from)
}
Converting the code to use a map increased the speed by around 20%, although the algorithm is still slow.
After trying to make the algorithm work faster, I'm out of ideas and I'm unsure why the expandToNeighbors function is taking a long time.
Implementation-wise it is quite messy unfortunately because of the whole list index to XYPosition conversions, but at least it works - the only problem is the performance.
So I have two one major problem.
I've actually pushed the fill tool to GitHub as a KIOL (Known Issue or Limitation), so the user can use the fill tool if they want, but they need to be aware of the limitations/issues. This is so anyone can have a look at my code and reproduce the bugs.
Link to repository:
https://github.com/realtomjoney/PyxlMoose
Edit
I understand that this question is extremely difficult to answer and will require a lot of thinking. I would recommend cloning PyxlMoose and reproduce the errors, then work from there. Relying on the code snippets isn't enough.
Formula for converting XY position to an index
Somebody in the comments suggested a formula for converting an XYPosition to an index value, I came up with the following method which works:
fun convertXYPositionToIndex(xyPosition: XYPosition, spanCount: Int): Int {
val positionX = xyPosition.x
val positionY = xyPosition.y
return (spanCount - positionY) + (spanCount * (positionX - 1))
}
The only problem is - it increases the speed by around 50% but it's still taking around 10-15 seconds to fill in an area of 80 by 80 pixels, so it has helped to a large degree although it's still very slow.
I think the performance issue is because of expandToNeighbors method generates 4 points all the time. It becomes crucial on the border, where you'd better generate 3 (or even 2 on corner) points, so extra point is current position again. So first border point doubles following points count, second one doubles it again (now it's x4) and so on.
If I'm right, you saw not the slow method work, but it was called too often.
How I fixed it:
Getting rid of the toList() calls.
Creating an convertXYPositionToIndex() function.
Here is my new code:
Tools.FILL_TOOL -> {
val seedColor = instance.rectangles[rectTapped]?.color ?: Color.WHITE
val queue = LinkedList<XYPosition>()
val spanCount = instance.spanCount.toInt()
queue.offer(MathExtensions.convertIndexToXYPosition(rectangleData.indexOf(rectTapped), spanCount))
val selectedColor = getSelectedColor()
while (queue.isNotEmpty() && seedColor != selectedColor) {
val current = queue.poll()
val color = instance.rectangles[rectangleData[convertXYDataToIndex(spanCount, current)]]?.color ?: Color.WHITE
if (color != seedColor) {
continue
}
instance.rectangles[rectangleData[convertXYDataToIndex(spanCount, current)]] = defaultRectPaint // Colors in pixel with defaultRectPaint
instance.extraCanvas.drawRect(rectangleData[MathExtensions.convertXYPositionToIndex(current, spanCount)], defaultRectPaint)
for (index in expandToNeighborsWithMap(spanCount, current)) {
val candidate = MathExtensions.convertIndexToXYPosition(index, spanCount)
queue.offer(candidate)
}
}
val timeTakenForThis = (System.currentTimeMillis()-startTime)
totalTime += timeTakenForThis
}
Expand to neighbors func:
fun expandToNeighborsWithMap(spanCount: Int, from: XYPosition): List<Int> {
val toReturn = mutableListOf<Int>()
if (from.x > 1) {
toReturn.add(MathExtensions.convertXYPositionToIndex(XYPosition(from.x - 1, from.y), spanCount))
}
if (from.x < spanCount) {
toReturn.add(MathExtensions.convertXYPositionToIndex(XYPosition(from.x + 1, from.y), spanCount))
}
if (from.y > 1) {
toReturn.add(MathExtensions.convertXYPositionToIndex(XYPosition(from.x, from.y - 1), spanCount))
}
if (from.y < spanCount) {
toReturn.add(MathExtensions.convertXYPositionToIndex(XYPosition(from.x, from.y + 1), spanCount))
}
return toReturn
}
It takes less than a second for canvas sizes of 100 by 100 and 200 by 200, so I'd say it's in the usable stage now.
I would say this is one of the simplest Android flood fill algorithms out there to understand, so if anyone is making an app similar to mine and they want a flood fill tool they can copy my code.
A guy in the comments called EvilTalk helped me with this.
I have an app in which gives you a certain photo based on an integer being fetched via
class RandomImageLogic(){
fun retrive(): Int{
return (1..9).random()
}
}
However, it is not professional to have a repeated outcome, as I desire a random integer to be fetched each time I call the function in order for the image to be different each time my button is pressed. How can I fetch a new random integer whenever the button calls the function?
The easy way is to pass in the last random number you received and filter it out.
fun retrive(except: Int): Int{
return ((1..9).filter {it != except}).random();
}
In your case, this method might not be called extremely often (only when the user clicks a button).
If this method was to be called more often, filter on an IntRange should be used with care (as proposed in #avalerio's answer).
This iterates the whole range (unnecessarily costing time) and it would create a temporary ArrayList on every call (creating unnecessary garbage and triggering the garbage collector more often than needed).
Here is a sample object NonRepeatingRandom (you can also implement it as a class if you wish). retrieve (which has been expanded with a max parameter and basic sanity checks) recursivly calls itself again if the same number would be generated twice in a row:
object NonRepeatingRandom {
private var previous = -1
fun retrieve(max : Int = 9): Int {
if(max < 0) {
error("Only positive numbers")
}
if(max <= 1) {
// There is nothing random about 0 or 1, do not check against previous, just return
previous = max
return max
}
val rand = (1..9).random()
return if(rand == previous) {
retrieve(max) // recursive call if two subsequent retrieve() calls would return the same number
} else {
previous = rand // remember last random number
rand
}
}
}
fun main(args: Array<String>) {
repeat(1000) {
println(NonRepeatingRandom.retrieve())
}
}
I made a quick-and-dirty performance test, calling my "recursive" method 10 million times and calling the "filter" method 10 million times.
Recursive: 125 ms (10 mio calls)
Filter: 864 ms (10 mio calls)
Pre-fill & random shuffle approach:
class RandomIntIterator(
private val range: IntRange
) : Iterator<Int> {
private lateinit var iterator: Iterator<Int>
init { randomize() }
fun randomize() {
iterator = range.shuffled().iterator()
}
override fun hasNext() = iterator.hasNext()
override fun next() = iterator.next()
}
...
val rnd = RandomIntIterator(1..9)
...
// on button click
if (rnd.hasNext()) {
val num = rnd.next()
// use num
} else {
// renew (if needed)
rnd.randomize()
}
I like using Sequences to generate unending streams of values.
In this case we'll have to write custom code, as checking for repeated values is a stateful operation, and while Sequences has a distinct() stateful filter, it applies to all generated values - we only want it to apply to a limited window.
TL;DR:
class RandomImageLogic(
private val random: Random,
/** The number of sequential values that must be distinct */
noRepeatsLimit: Int = 2
) {
private val sourceValues: List<Int> = (0..9).toList()
private fun nextValue(vararg exclusions: Int): Int =
(sourceValues - exclusions.asList()).random(random)
private val randomInts: Iterator<Int> =
generateSequence({
// the initial value just has one random int
val next = nextValue()
ArrayDeque(listOf(next))
}) { previousValues ->
// generate the next value, excluding previous values
val nextValue = nextValue(*previousValues.toIntArray())
// limit the size of previousValues, if necessary
if (previousValues.size >= noRepeatsLimit)
previousValues.removeLastOrNull()
// add the generated value to the beginning of the deque
previousValues.addFirst(nextValue)
previousValues
}
.map {
// convert the Sequence to a list of ints,
// each element is the first item in the deque
it.first()
}
.iterator()
fun retrieve(): Int {
return randomInts.next()
}
}
Testing
Let's write a test first, to make sure our solution works. Kotest has a specific property based testing subproject, and this will let us cover a wide range of test cases very quickly.
So, I ran through the setup, and started setting up the test case.
Seeding RandomImageLogic
First I modified the RandomImageLogic class so that the random selection could be seeded with a provided Random.
import kotlin.random.Random
class RandomImageLogic(private val random: Random) {
fun retrieve(): Int {
return (1..9).random(random = random)
}
}
This will help us to create a Generator for the RandomImageLogic.
Testing all values
Now we can use Kotest to write a property-based test that will assert "for all sequential values, they are different"
import io.kotest.core.spec.style.FunSpec
import io.kotest.property.arbitrary.arbitrary
import io.kotest.property.forAll
class RandomImageLogicTest: FunSpec({
// This generator will create a new instance of `RandomImageLogic`
// and generate two sequential values.
val sequentialValuesArb = arbitrary { rs ->
val randomImageLogic = RandomImageLogic(rs.random)
val firstValue = randomImageLogic.retrieve()
val secondValue = randomImageLogic.retrieve()
firstValue to secondValue
}
test("expect sequential values are different") {
forAll(sequentialValuesArb) { (firstValue, secondValue) ->
firstValue != secondValue
}
}
})
Of course, the test fails.
Property failed after 4 attempts
Arg 0: (1, 1)
Repeat this test by using seed 1210584330919845105
Caused by org.opentest4j.AssertionFailedError: expected:<true> but was:<false>
So let's fix it!
Generating Sequences
As I said earlier, I really like Sequences. They're perfect for this use-case, where we have an infinite source of values.
To demonstrate how to make a Sequence, let's convert the existing code, and use an Iterator to fetch values.
class RandomImageLogic(private val random: Random) {
private val randomInts =
// generate a sequence using values from this lambda
generateSequence { (1..9).random(random = random) }
// use an iterator to fetch values
.iterator()
fun retrieve(): Int {
return randomInts.next()
}
}
This hasn't solved the problem yet - the Sequence only generates and provides one value at a time and so cannot do any filtering. Fortunately generateSequence() has an variant with nextFunction: (T) -> T?, where we can determine the next value based on the previous value.
If we use this constructor, and do a bit of refactoring to share the source values, and a util method to generate a next value while filtering out previous values...
private val sourceValues: List<Int> = (0..9).toList()
private fun nextValue(vararg exclusions: Int): Int =
(sourceValues - exclusions.asList()).random(random)
private val randomInts: Iterator<Int> =
generateSequence({ nextValue() }) { previousValue ->
nextValue(previousValue)
}
.iterator()
and now if we run the test, it passes!
Test Duration Result
expect sequential values are different 0.077s passed
Improvement: more than two distinct sequential values
What happens if you don't just want two sequential values to be distinct, but 3? Or even more? Let's make the 'no-repeated-values' limit configurable, and I think this will demonstrate why Sequences are a good solution.
class RandomImageLogic(
private val random: Random,
/** The number of sequential values that must be distinct */
noRepeatsLimit: Int = 2
) {
// ...
}
Testing
Once again, let's write a test to make sure things work as expected.
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.collections.shouldNotContainDuplicates
import io.kotest.property.Arb
import io.kotest.property.arbitrary.int
import io.kotest.property.checkAll
import kotlin.random.Random
class RandomImageLogicTest : FunSpec({
test("expect arbitrary sequential values are different") {
checkAll(Arb.int(), Arb.int(1..10)) { seed, noRepeatsLimit->
val randomImageLogic = RandomImageLogic(Random(seed), noRepeatsLimit)
val result = List(noRepeatsLimit) { randomImageLogic.retrieve() }
withClue("Result: $result") {
result shouldHaveSize noRepeatsLimit
result.shouldNotContainDuplicates()
}
}
}
})
And of course the test fails.
Property test failed for inputs
0) -459964888
1) 5
Caused by java.lang.AssertionError: Result: [3, 8, 0, 2, 8]
Collection should not contain duplicates
There's lots of options to make a Sequence stateful - again let's just look at one.
Sequence of values
Instead of a sequence of individual values, we can have a sequence where each element is a list of not only the current value, but also previously seen values.
Let's use ArrayDeque to store these values, because it's easy to add and remove values from the start and end.
Again, we use the same generateSequence constructor with a seedFunction and nextFunction - except this time each element is deque which stores all values, and in nextFunction we add new values to the start of the deque, trimming it if it's larger than the window size noRepeatsLimit
private val randomInts: Iterator<Int> =
generateSequence({
// the initial value just has one random int
val next = nextValue()
ArrayDeque(listOf(next))
}) { previousValues ->
// generate the next value, excluding previous values
val nextValue = nextValue(*previousValues.toIntArray())
// limit the size of previousValues, if necessary
if (previousValues.size >= noRepeatsLimit)
previousValues.removeLastOrNull()
// add the generated value to the beginning of the deque
previousValues.addFirst(nextValue)
previousValues
}
.map {
// convert the Sequence to a list of ints,
// each element is the first item in the deque
it.first()
}
.iterator()
And yup, the test passes!
Test Duration Result
expect arbitrary sequential values are different 0.210s passed
Storing state
It's important to think about how state will be stored. 'State' is required by RandomImageLogic to know whether a generated value is distinct.
In the Sequence implementation, it's stored internally, and so is specifically associated with an instance of RandomImageLogic. Maybe your application only has one instance of RandomImageLogic at any one time, in which case the state will always be up to date and will be shared between all invocations.
But what happens if there's more than one instance of RandomImageLogic? Or if there's multi-threading? Or if the RandomImageLogic instance is recreated?
The answers to these depend on the implementation and situation. From your question I suspect that it's not critically important that images never repeat, but I bring this up because it is important to think about.
In Android-Kotlin I am getting float number from backend (for example num = 10000000.47)
When I try to String.format it and add that number in my balanceTextview it shows it with exponent (something like 1.0E10).
I want to show number normally without exponent and with 2 decimals. (Without presicion loss!)
Tried to use DecimalFormat("#.##") but it didn't help me. Maybe I'm doing something wrong?
num = 10000000.47f
val dec = DecimalFormat("#.##")
var result = dec.format(num)
my result is: 10000000
It losts my decimal places
The issue is your number type. According to the documentation:
For variables initialized with fractional numbers, the compiler infers the Double type. To explicitly specify the Float type for a value, add the suffix f or F. If such a value contains more than 6-7 decimal digits, it will be rounded.
With an example that shows how information may get lost:
val pi = 3.14 // Double
val e = 2.7182818284 // Double
val eFloat = 2.7182818284f // Float, actual value is 2.7182817
If the value is specified as Double instead of Float, i.e.
val num = 10000000.47
instead of
val num = 10000000.47f
then your approach works as expected, but could be shortened to:
"%.2f".format(num)
(note that the shorter version will also print "100" as "100.00" which is different from your approach but potentially still desired behaviour)
If you receive a Float from the backend then the information is already lost on your side. Otherwise you should be able to fix the issue by improved parsing.
The extension function format is only available in the JVM. In Kotlin/native, you can use this instead:
fun Float.toPrecision(precision: Int) =
this.toDouble().toPrecision(precision)
fun Double.toPrecision(precision: Int) =
if (precision < 1) {
"${this.roundToInt()}"
} else {
val p = 10.0.pow(precision)
val v = (abs(this) * p).roundToInt()
val i = floor(v / p)
var f = "${floor(v - (i * p)).toInt()}"
while (f.length < precision) f = "0$f"
val s = if (this < 0) "-" else ""
"$s${i.toInt()}.$f"
}
I am summing double value from arraylist its giving additional decimals as 99999, how to fix this, please guide
ex
class ExDet{var expName:String ="",var expAmount:Double = 0.0}
val arrayList = ArrayList<ExDet>()
arrayList.add(ExDet("Abc 1",45.66))
arrayList.add(ExDet("DEF 1",10.0))
arrayList.add(ExDet("Lee 1",600.89))
arrayList.add(ExDet("Ifr 1",200.9))
var amt = arrayList.sumByDouble{ it.expAmount }
Expected Value of Amount is :
Amt = 857.45
But it returns
Amt = 857.4499999
Sample Code to Test
data class ExDet(var expName:String ="" ,var expAmount:Double=0.0)
fun main(args: Array<String>) {
val arrayList = ArrayList<ExDet>()
arrayList.add(ExDet("Abc 1",45.66))
arrayList.add(ExDet("DEF 1",10.0))
arrayList.add(ExDet("Lee 1",600.89))
arrayList.add(ExDet("Ifr 1",200.9))
var amt = arrayList.sumByDouble{ it.expAmount }
println("Amount is : $amt")
}
The issue you are confronted with is that floating point numbers are build on top of base 2, not base 10.
Think how you can easily represent a third as a fraction (1/3), but when you convert to decimal you get a repeating (recurring) number after the radix point (i.e. 0.33...). Some decimal numbers are recurring when represented in base-2, e.g. x.9. The computer has a finite number of bits, so the (base-2) number is truncated. All the truncation errors can add up.
You need to round to the required precision (e.g. round(x * 100) / 100).
If you are only interested in how it is displayed then you can use the format function with something like "%.2f".
String.format("%.2f", value)
I'm trying to convert a BLE native Byte handling helper to Kotlin. In the process, I noticed that in
JAVA
myByteArray[offset] += (byte)(exponent & 0xFF);
it works as expected, but when converted to kotlin
KOTLIN
myByteArray[offset] += (exponent and 0x0F shl 4).toByte()
I get errors that an Integer is expected.
So I assume the
"+="
is my problem and causing an integer to be assumed.
So I have two questions.
1) What exactly does += do with bytes. I understand that
b += 1
is the equivelent of
b = (byte)(b + 1)
but what exactly is happening to the bytes. Is there shifting going on, or is it converting to an int, adding the values then back to a byte?
2) What is the equivelent in Kotlin and why is it failing in Kotlin. I also tried doing
myByteArray[offset] = myByteArray[offset] + (exponent and 0x0F shl 4).toByte()
For the record, this is converting an Integer value into a 32bit Float if you are curious. Not sure if that helps at all.
The full code if you are interested for this is:
mantissa = intToSignedBits(mantissa, 12)
exponent = intToSignedBits(exponent, 4)
myByteArray[offset++] = (mantissa and 0xFF).toByte()
myByteArray[offset] = (mantissa shr 8 and 0x0F).toByte()
myByteArray[offset] += (exponent and 0x0F shl 4).toByte() //hates this line
NOTE* It says
this
because I wrote it as a ByteArray extension, so think of this as the byteArray itself.
KOTLIN version of it (ERRORS)
JAVA version of it (NO ERRORS):
I desire more than just an answer, but I'll take it lol. I would like to understand a bit more of what is going on here so I can solve this on my own in the future as well. Thanks in advance to any who take the time to explain and help.
Also while we are on the subject ;).
private int unsignedByteToInt(byte b) {
return b & 0xFF;
}
why does this work in Java, but fail in Kotlin.
private fun unsignedByteToInt(b: Byte): Int {
return b and 0xFF
}
Gotta be honest, I'm tired of writing Java helper classes to handle bytes, so I'm trying to figure out the idioms of Kotlin byte handling. "and" seems to only have overloads for Ints and bytes are not treated as Ints in Kotlin.
So as a bonus question, if you can explain that one, I would also appreciate it.
I believe you should be using Byte#plus(Byte).
So in your case:
myByteArray[offset] = myByteArray[offset].plus((exponent and 0x0F shl 4).toByte())
.toByte() probably isn't necessary in this case, since plus() takes basically any number, but I'm not able to test it.
Did You notice that breaking down your statement works?
val newBytes = (myByteArray[offset]+(exponent and 0x0F shl 4)).toByte()
myByteArray[offset] = newBytes
Main culprit of this behavior is plus operator signature. Those are all the overloads of plus for Byte:
/** Adds the other value to this value. */
public operator fun plus(other: Byte): Int
/** Adds the other value to this value. */
public operator fun plus(other: Short): Int
/** Adds the other value to this value. */
public operator fun plus(other: Int): Int
/** Adds the other value to this value. */
public operator fun plus(other: Long): Long
/** Adds the other value to this value. */
public operator fun plus(other: Float): Float
/** Adds the other value to this value. */
public operator fun plus(other: Double): Double
None of those return a Byte and there are no overloads of plusAssign, so they are implicitly created.
First it performs plus(Byte) returning Int then tries assignment, but it requires a Byte so it causes your error.