Merge pull request #3160 from davotoula/chore/remove-tarsosdsp-gpl

Replace GPLv3 TarsosDSP with an in-house pitch shifter
This commit is contained in:
Vitor Pamplona
2026-06-09 17:09:11 -04:00
committed by GitHub
7 changed files with 367 additions and 142 deletions
+31
View File
@@ -189,6 +189,37 @@ version. `quartz/` is protocol-only — no composables.
./gradlew spotlessApply
```
## Dependency Licensing
**MANDATORY whenever you introduce a new third-party dependency** — in *any*
module (`quartz`, `commons`, `amethyst`, `desktopApp`, `cli`, `quic`,
`nestsClient`, …), whether you add it to `gradle/libs.versions.toml` or to a
module's `build.gradle.kts`: determine its license **before** wiring it in.
Amethyst ships under the **MIT** license, so a copyleft dependency linked into a
distributed artifact (APK, desktop binary) can force that artifact's
combined-work terms onto the whole project.
Verify against the dependency's actual `LICENSE`/`COPYING` file or its published
POM — **not from memory**. Then classify and act:
- **Permissive** (MIT, Apache-2.0, BSD, ISC, MPL-2.0, zlib, …) → **OK**,
proceed.
- **LGPL, or GPL/EPL with a linking / Classpath exception** → **WARN.**
Acceptable to link (the exception keeps our own code MIT), but call it out in
your summary so the human knows. Confirm the exception actually exists in the
LICENSE text — don't assume it does.
- **Stricter than LGPL** — GPL/AGPL **without** a linking exception, SSPL,
proprietary/commercial-only, or anything where the linking-exception check is
"no" → **STRONGLY WARN and STOP.** Do not add it silently. Surface it
prominently and **require an explicit call-out in the PR description** so a
maintainer makes the decision. Prefer a permissive alternative, a clean-room
implementation, or dropping the feature.
For any GPL-family hit the decisive question is always **"is there a linking
(LGPL/Classpath) exception?"** — that is what separates a WARN from a STOP.
(Example: TarsosDSP, GPLv3 with no exception, was removed from `amethyst` and
replaced with an in-house pitch shifter for exactly this reason.)
## Quartz KMP Structure
Quartz uses expect/actual for platform-specific implementations (e.g. crypto
-3
View File
@@ -474,9 +474,6 @@ dependencies {
// EXIF metadata stripping
implementation(libs.androidx.exifinterface)
// Voice anonymization DSP
implementation(libs.tarsosdsp)
// WebRTC for voice/video calls
implementation(libs.stream.webrtc.android)
@@ -0,0 +1,178 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.uploads
import com.vitorpamplona.amethyst.commons.audio.AudioWindow
import kotlin.math.roundToInt
/**
* Pure-Kotlin pitch shifter for mono float PCM.
*
* Shifts pitch by a frequency [ratio][shift] while preserving the original
* duration, by combining two textbook DSP steps:
*
* 1. **WSOLA time-stretch** (Waveform Similarity Overlap-Add) stretches the
* signal in time by the pitch ratio without changing its pitch. Unlike plain
* overlap-add, WSOLA searches a small window around each analysis frame for
* the offset that best correlates with the natural continuation of the
* previous frame, which avoids the phase-discontinuity "warble" of OLA.
* 2. **Linear resampling** back to the original length raises (or lowers) the
* pitch by the same ratio and restores the duration.
*
* This is a clean-room implementation of the standard WSOLA algorithm; it
* carries no third-party code and keeps the app free of GPL-licensed DSP
* dependencies. Quality is more than sufficient for voice anonymization, whose
* pitch factors are modest (~0.7x1.5x).
*/
class PitchShifter(
/** Analysis/synthesis window length in samples. ~46 ms at 44.1 kHz. */
private val frameSize: Int = 2048,
/** Half-radius (in samples) of the WSOLA cross-correlation search. */
private val seekWindow: Int = 256,
) {
private val synthesisHop = frameSize / 2
private val overlap = frameSize - synthesisHop
private val window = AudioWindow.hann(frameSize)
/**
* Returns [input] pitch-shifted by [frequencyRatio] with the same length.
*
* @param frequencyRatio output-to-input frequency multiplier. `> 1` raises
* the pitch, `< 1` lowers it, `1.0` is a no-op copy.
*/
fun shift(
input: FloatArray,
frequencyRatio: Double,
): FloatArray {
if (input.isEmpty()) return FloatArray(0)
if (frequencyRatio == 1.0) return input.copyOf()
// Stretch in time by the ratio (pitch unchanged, length scaled), then
// resample back to the original length (pitch scaled by the ratio).
val stretched = timeStretch(input, frequencyRatio)
return resampleLinear(stretched, input.size)
}
/**
* WSOLA time-stretch. Output length is approximately `input.size * stretch`.
*/
private fun timeStretch(
input: FloatArray,
stretch: Double,
): FloatArray {
if (input.size < frameSize + seekWindow) {
// Too short for the overlap machinery; resampling alone still yields
// the requested length change without crashing on tiny clips.
return resampleLinear(input, (input.size * stretch).roundToInt().coerceAtLeast(1))
}
val analysisHop = synthesisHop / stretch
val outLength = (input.size * stretch).roundToInt() + frameSize
val out = FloatArray(outLength)
val norm = FloatArray(outLength)
// Integer start of the current analysis frame inside the input.
var analysisStart = 0
// Fractional nominal position; the next frame is sought around it.
var nominal = 0.0
var synthesisPos = 0
while (analysisStart + frameSize < input.size && synthesisPos + frameSize < outLength) {
// Overlap-add the windowed analysis frame at the synthesis position.
for (i in 0 until frameSize) {
val w = window[i]
out[synthesisPos + i] += input[analysisStart + i] * w
norm[synthesisPos + i] += w
}
synthesisPos += synthesisHop
nominal += analysisHop
// The samples that should naturally follow what we just wrote: the
// tail of the current frame advanced by one synthesis hop.
val naturalStart = analysisStart + synthesisHop
if (naturalStart + overlap >= input.size) break
// Search around the nominal next analysis position for the offset
// whose head best correlates with that natural continuation.
val center = nominal.roundToInt()
analysisStart = bestMatchOffset(input, naturalStart, center)
}
// Normalize where windows overlapped to keep unity gain at the edges.
for (i in out.indices) {
if (norm[i] > 1e-6f) out[i] /= norm[i]
}
// Trim the padding tail to the expected stretched length.
val expected = (input.size * stretch).roundToInt()
return if (expected < out.size) out.copyOf(expected) else out
}
/**
* Finds, within ±[seekWindow] of [center], the input offset whose
* `overlap`-length head best cross-correlates with the `overlap`-length
* segment starting at [naturalStart]. Returns a bounds-safe offset.
*/
private fun bestMatchOffset(
input: FloatArray,
naturalStart: Int,
center: Int,
): Int {
val low = (center - seekWindow).coerceAtLeast(0)
val high = (center + seekWindow).coerceAtMost(input.size - frameSize - 1)
if (high <= low) return low.coerceIn(0, input.size - frameSize - 1)
var bestOffset = low
var bestCorr = Double.NEGATIVE_INFINITY
for (offset in low..high) {
var corr = 0.0
for (i in 0 until overlap) {
corr += input[naturalStart + i].toDouble() * input[offset + i]
}
if (corr > bestCorr) {
bestCorr = corr
bestOffset = offset
}
}
return bestOffset
}
/** Linear-interpolation resample of [input] to exactly [targetLength] samples. */
private fun resampleLinear(
input: FloatArray,
targetLength: Int,
): FloatArray {
if (targetLength <= 0 || input.isEmpty()) return FloatArray(0)
if (input.size == 1) return FloatArray(targetLength) { input[0] }
val out = FloatArray(targetLength)
val step = (input.size - 1).toDouble() / (targetLength - 1).coerceAtLeast(1)
for (i in 0 until targetLength) {
val pos = i * step
val idx = pos.toInt()
val frac = pos - idx
val a = input[idx]
val b = if (idx + 1 < input.size) input[idx + 1] else a
out[i] = (a + (b - a) * frac).toFloat()
}
return out
}
}
@@ -25,19 +25,14 @@ import android.media.MediaCodecInfo
import android.media.MediaExtractor
import android.media.MediaFormat
import android.media.MediaMuxer
import be.tarsos.dsp.AudioDispatcher
import be.tarsos.dsp.AudioEvent
import be.tarsos.dsp.AudioProcessor
import be.tarsos.dsp.WaveformSimilarityBasedOverlapAdd
import be.tarsos.dsp.io.TarsosDSPAudioFloatConverter
import be.tarsos.dsp.io.TarsosDSPAudioFormat
import be.tarsos.dsp.resample.RateTransposer
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
import java.io.File
import java.nio.ByteBuffer
import java.nio.ByteOrder
import kotlin.math.abs
@@ -57,9 +52,10 @@ data class AnonymizedResult(
/**
* Processes audio files to alter voice characteristics for privacy.
*
* Uses TarsosDSP's WSOLA (Waveform Similarity Overlap-Add) algorithm combined with
* rate transposition to shift pitch while preserving duration. Note that in TarsosDSP,
* pitch factors work inversely: factor < 1 raises pitch, factor > 1 lowers pitch.
* Uses the in-house [PitchShifter] (WSOLA time-stretch + resampling) to shift pitch
* while preserving duration. Preset pitch factors work inversely: factor < 1 raises
* pitch, factor > 1 lowers pitch — so the shifter receives the reciprocal as its
* output-to-input frequency ratio.
*/
class VoiceAnonymizer {
companion object {
@@ -73,7 +69,7 @@ class VoiceAnonymizer {
*
* The process involves three stages:
* 1. Decode input audio to PCM (0-30% progress)
* 2. Apply pitch shifting with TarsosDSP (30-70% progress)
* 2. Apply pitch shifting with [PitchShifter] (30-70% progress)
* 3. Encode processed audio to AAC (70-100% progress)
*
* @param inputFile Source audio file (supports formats decodable by MediaCodec)
@@ -101,10 +97,8 @@ class VoiceAnonymizer {
onProgress(progress * 0.3f)
}
val processedPcm =
processPcmWithTarsos(pcmData, preset, sampleRate) { progress ->
onProgress(0.3f + progress * 0.4f)
}
val processedPcm = applyPitchShift(pcmData, preset)
onProgress(0.7f)
val waveform = extractWaveform(processedPcm, sampleRate)
@@ -114,6 +108,10 @@ class VoiceAnonymizer {
onProgress(1f)
Result.success(AnonymizedResult(outputFile, waveform, duration))
} catch (e: CancellationException) {
// Let cancellation propagate so structured concurrency works (e.g. the
// upload screen being dismissed cancels this job).
throw e
} catch (e: Exception) {
Log.e(TAG, "Failed to anonymize audio", e)
Result.failure(e)
@@ -176,7 +174,10 @@ class VoiceAnonymizer {
val inputBufferIndex = decoder.dequeueInputBuffer(10000)
if (inputBufferIndex < 0) return
val inputBuffer = decoder.getInputBuffer(inputBufferIndex)!!
val inputBuffer =
requireNotNull(decoder.getInputBuffer(inputBufferIndex)) {
"Decoder input buffer $inputBufferIndex was null (codec in async mode?)"
}
val sampleSize = extractor.readSampleData(inputBuffer, 0)
if (sampleSize < 0) {
@@ -232,7 +233,10 @@ class VoiceAnonymizer {
}
private fun extractPcmSamples(outputBufferIndex: Int) {
val outputBuffer = decoder.getOutputBuffer(outputBufferIndex)!!
val outputBuffer =
requireNotNull(decoder.getOutputBuffer(outputBufferIndex)) {
"Decoder output buffer $outputBufferIndex was null (codec in async mode?)"
}
val shortBuffer = outputBuffer.order(ByteOrder.nativeOrder()).asShortBuffer()
while (shortBuffer.hasRemaining()) {
pcmSamples.add(shortBuffer.get() / 32768f)
@@ -292,11 +296,9 @@ class VoiceAnonymizer {
release()
}
private fun processPcmWithTarsos(
private fun applyPitchShift(
pcmData: FloatArray,
preset: VoicePreset,
sampleRate: Int,
onProgress: (Float) -> Unit,
): FloatArray {
val baseFactor = preset.pitchFactor
val factor =
@@ -311,75 +313,11 @@ class VoiceAnonymizer {
baseFactor
}
}
val totalSamples = pcmData.size
val processedSamples = ArrayList<Float>(totalSamples)
val wsola =
WaveformSimilarityBasedOverlapAdd(
WaveformSimilarityBasedOverlapAdd.Parameters.musicDefaults(
factor,
sampleRate.toDouble(),
),
)
val rateTransposer = RateTransposer(factor)
val bufferSize = wsola.inputBufferSize
val overlap = wsola.overlap
val tarsosDspFormat =
TarsosDSPAudioFormat(
sampleRate.toFloat(),
16,
1,
true,
false,
)
val collector =
object : AudioProcessor {
override fun process(audioEvent: AudioEvent): Boolean {
val buffer = audioEvent.floatBuffer
for (i in 0 until audioEvent.bufferSize) {
processedSamples.add(buffer[i])
}
return true
}
override fun processingFinished() {
// No-op: no cleanup needed
}
}
val dispatcher =
AudioDispatcher(
FloatArrayAudioInputStream(pcmData, tarsosDspFormat, pcmData.size.toLong()),
bufferSize,
overlap,
)
wsola.setDispatcher(dispatcher)
dispatcher.addAudioProcessor(wsola)
dispatcher.addAudioProcessor(rateTransposer)
dispatcher.addAudioProcessor(collector)
var samplesProcessed = 0
val progressProcessor =
object : AudioProcessor {
override fun process(audioEvent: AudioEvent): Boolean {
samplesProcessed += audioEvent.bufferSize
onProgress((samplesProcessed.toFloat() / totalSamples).coerceIn(0f, 1f))
return true
}
override fun processingFinished() {
// No-op: no cleanup needed
}
}
dispatcher.addAudioProcessor(progressProcessor)
dispatcher.run()
return processedSamples.toFloatArray()
// Presets express factor inversely (factor > 1 lowers pitch), so the
// output-to-input frequency ratio is the reciprocal.
val frequencyRatio = 1.0 / factor
return PitchShifter().shift(pcmData, frequencyRatio)
}
private fun extractWaveform(
@@ -421,7 +359,10 @@ class VoiceAnonymizer {
val inputBufferIndex = encoder.dequeueInputBuffer(10000)
if (inputBufferIndex < 0) return
val inputBuffer = encoder.getInputBuffer(inputBufferIndex)!!
val inputBuffer =
requireNotNull(encoder.getInputBuffer(inputBufferIndex)) {
"Encoder input buffer $inputBufferIndex was null (codec in async mode?)"
}
inputBuffer.clear()
val samplesToWrite = minOf((inputBuffer.capacity() / 2), pcmData.size - inputOffset)
@@ -445,7 +386,7 @@ class VoiceAnonymizer {
private fun queueSampleData(
inputBufferIndex: Int,
inputBuffer: java.nio.ByteBuffer,
inputBuffer: ByteBuffer,
samplesToWrite: Int,
) {
writePcmSamplesToBuffer(inputBuffer, samplesToWrite)
@@ -456,7 +397,7 @@ class VoiceAnonymizer {
}
private fun writePcmSamplesToBuffer(
inputBuffer: java.nio.ByteBuffer,
inputBuffer: ByteBuffer,
samplesToWrite: Int,
) {
for (i in 0 until samplesToWrite) {
@@ -497,13 +438,16 @@ class VoiceAnonymizer {
}
private fun processOutputBuffer(outputBufferIndex: Int) {
val outputBuffer = encoder.getOutputBuffer(outputBufferIndex)!!
val outputBuffer =
requireNotNull(encoder.getOutputBuffer(outputBufferIndex)) {
"Encoder output buffer $outputBufferIndex was null (codec in async mode?)"
}
writeToMuxerIfReady(outputBuffer)
encoder.releaseOutputBuffer(outputBufferIndex, false)
checkForEndOfStream()
}
private fun writeToMuxerIfReady(outputBuffer: java.nio.ByteBuffer) {
private fun writeToMuxerIfReady(outputBuffer: ByteBuffer) {
if (isMuxerStarted && bufferInfo.size > 0) {
outputBuffer.position(bufferInfo.offset)
outputBuffer.limit(bufferInfo.offset + bufferInfo.size)
@@ -524,7 +468,7 @@ class VoiceAnonymizer {
setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE)
}
private fun encodePcmToAac(
private suspend fun encodePcmToAac(
pcmData: FloatArray,
sampleRate: Int,
outputFile: File,
@@ -541,7 +485,7 @@ class VoiceAnonymizer {
val inputFeeder = EncoderInputFeeder(encoder, pcmData, sampleRate, onProgress)
val outputDrainer = EncoderOutputDrainer(encoder, muxer)
while (!outputDrainer.isDone) {
while (!outputDrainer.isDone && currentCoroutineContext().isActive) {
inputFeeder.feedInput()
outputDrainer.drainOutput()
}
@@ -559,44 +503,3 @@ class VoiceAnonymizer {
release()
}
}
private class FloatArrayAudioInputStream(
private val floatArray: FloatArray,
private val format: TarsosDSPAudioFormat,
private val frameLength: Long,
) : be.tarsos.dsp.io.TarsosDSPAudioInputStream {
private var position = 0
override fun getFormat(): TarsosDSPAudioFormat = format
override fun getFrameLength(): Long = frameLength
override fun read(
buffer: ByteArray,
offset: Int,
length: Int,
): Int {
val converter = TarsosDSPAudioFloatConverter.getConverter(format)
val floatBuffer = FloatArray(length / 2)
val samplesToRead = minOf(floatBuffer.size, floatArray.size - position)
if (samplesToRead <= 0) return -1
System.arraycopy(floatArray, position, floatBuffer, 0, samplesToRead)
position += samplesToRead
converter.toByteArray(floatBuffer, samplesToRead, buffer, offset)
return samplesToRead * 2
}
override fun skip(bytesToSkip: Long): Long {
val samplesToSkip = (bytesToSkip / 2).toInt()
val actualSkip = minOf(samplesToSkip, floatArray.size - position)
position += actualSkip
return actualSkip.toLong() * 2
}
override fun close() {
// No-op: no cleanup needed
}
}
@@ -0,0 +1,119 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.uploads
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import kotlin.math.PI
import kotlin.math.sin
class PitchShifterTest {
private val sampleRate = 44100
/** One second of a clean mono sine at [freq] Hz, amplitude 0.8. */
private fun sine(
freq: Double,
seconds: Double = 1.0,
): FloatArray {
val n = (sampleRate * seconds).toInt()
return FloatArray(n) { i -> (0.8 * sin(2.0 * PI * freq * i / sampleRate)).toFloat() }
}
/**
* Estimates the fundamental frequency of a (near-)periodic signal via
* autocorrelation over a stable middle window. Dependency-free and robust
* for sine inputs.
*/
private fun estimateFrequency(signal: FloatArray): Double {
// Use the central 60% to avoid edge ramp-up/ramp-down artifacts.
val start = (signal.size * 0.2).toInt()
val end = (signal.size * 0.8).toInt()
val chunk = signal.copyOfRange(start, end)
val minFreq = 50.0
val maxFreq = 2000.0
val minLag = (sampleRate / maxFreq).toInt()
val maxLag = (sampleRate / minFreq).toInt().coerceAtMost(chunk.size - 1)
var bestLag = minLag
var bestCorr = Double.NEGATIVE_INFINITY
for (lag in minLag..maxLag) {
var corr = 0.0
for (i in 0 until chunk.size - lag) {
corr += chunk[i] * chunk[i + lag]
}
if (corr > bestCorr) {
bestCorr = corr
bestLag = lag
}
}
return sampleRate.toDouble() / bestLag
}
@Test
fun `ratio of one preserves length`() {
val input = sine(440.0)
val output = PitchShifter().shift(input, 1.0)
// Duration must be preserved (within a frame of tolerance).
assertEquals(input.size.toDouble(), output.size.toDouble(), 4096.0)
}
@Test
fun `shift up one octave roughly doubles the fundamental`() {
val output = PitchShifter().shift(sine(440.0), 2.0)
val measured = estimateFrequency(output)
assertEquals(880.0, measured, 880.0 * 0.06)
}
@Test
fun `shift down one octave roughly halves the fundamental`() {
val output = PitchShifter().shift(sine(440.0), 0.5)
val measured = estimateFrequency(output)
assertEquals(220.0, measured, 220.0 * 0.06)
}
@Test
fun `preserves duration for the deep preset ratio`() {
// DEEP preset lowers pitch: frequencyRatio = 1 / 1.4.
val input = sine(300.0)
val output = PitchShifter().shift(input, 1.0 / 1.4)
assertEquals(input.size.toDouble(), output.size.toDouble(), 4096.0)
val measured = estimateFrequency(output)
assertEquals(300.0 / 1.4, measured, (300.0 / 1.4) * 0.06)
}
@Test
fun `preserves duration for the high preset ratio`() {
// HIGH preset raises pitch: frequencyRatio = 1 / 0.75.
val input = sine(300.0)
val output = PitchShifter().shift(input, 1.0 / 0.75)
assertEquals(input.size.toDouble(), output.size.toDouble(), 4096.0)
val measured = estimateFrequency(output)
assertEquals(300.0 / 0.75, measured, (300.0 / 0.75) * 0.06)
}
@Test
fun `empty input returns empty output`() {
val output = PitchShifter().shift(FloatArray(0), 1.5)
assertTrue(output.isEmpty())
}
}
-2
View File
@@ -60,7 +60,6 @@ securityCryptoKtx = "1.1.0"
slf4j = "2.0.18"
spotless = "8.6.0"
streamWebrtcAndroid = "1.3.10"
tarsosdsp = "2.5"
translate = "17.0.3"
jetbrainsCompose = "1.11.0"
unifiedpush = "3.0.10"
@@ -198,7 +197,6 @@ secp256k1-kmp-jni-android = { group = "fr.acinq.secp256k1", name = "secp256k1-km
secp256k1-kmp-jni-jvm = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-jvm", version.ref = "secp256k1KmpJniAndroid" }
schnorr256k1-kmp = { group = "com.vitorpamplona.schnorr256k1", name = "schnorr256k1-kmp", version.ref = "schnorr256k1Kmp" }
stream-webrtc-android = { group = "io.getstream", name = "stream-webrtc-android", version.ref = "streamWebrtcAndroid" }
tarsosdsp = { group = "be.tarsos.dsp", name = "core", version.ref = "tarsosdsp" }
unifiedpush = { group = "com.github.UnifiedPush", name = "android-connector", version.ref = "unifiedpush" }
play-services-cast-framework = { group = "com.google.android.gms", name = "play-services-cast-framework", version.ref = "playServicesCast" }
vico-charts-compose = { group = "com.patrykandpatrick.vico", name = "compose", version.ref = "vico-charts-compose" }
-1
View File
@@ -26,7 +26,6 @@ dependencyResolutionManagement {
mavenCentral()
maven { url = uri("https://jitpack.io") }
maven { url = uri("https://raw.githubusercontent.com/guardianproject/gpmaven/master") }
maven { url = uri("https://mvn.0110.be/releases") }
}
}