From 6491288513baab95772dbe2c2260c9e06005dbc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=9ET=CE=9ERNAL?= Date: Sun, 29 Mar 2026 12:35:34 +0530 Subject: [PATCH] v1.6.1 --- build.gradle.kts | 4 +- changelogs-stable.txt | 5 + common/src/main/assets/lang/en_US.json | 5 +- .../common/config/impl/MessagingTweaks.kt | 6 +- .../impl/downloader/MediaDownloader.kt | 236 +-------------- .../messaging/ConversationSoundEffects.kt | 271 ++++++++++++++---- .../core/features/impl/tweaks/CameraTweaks.kt | 13 +- gradle.properties | 4 +- 8 files changed, 242 insertions(+), 302 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 6512bc14..81497cdd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,8 +33,8 @@ tasks.register("getVersion") { } // You can still set these for legacy use by submodules or scripts: -rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.6.0").get()) -rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("310").get().toInt()) +rootProject.ext.set("appVersionName", providers.gradleProperty("APP_VERSION_NAME").orElse("1.6.1").get()) +rootProject.ext.set("appVersionCode", providers.gradleProperty("APP_VERSION_CODE").orElse("312").get().toInt()) rootProject.ext.set("applicationId", "me.eternal.purrfectsnap") // buildHash: when PurrfectSnap or Snapchat is updated, mappings become outdated and auto-regenerate. // Include version code so each release has a different hash; use random for uniqueness within same version. diff --git a/changelogs-stable.txt b/changelogs-stable.txt index 987983a9..57fabf97 100644 --- a/changelogs-stable.txt +++ b/changelogs-stable.txt @@ -1,3 +1,8 @@ +## v1.6.1 +- Fix: Custom Frame Rate +- Fix: Conversation Sound Style +- Fix: Auto Reactions when downloading stories(Removed story thumbnail feature) + ## v1.6.0 - New: Conversation Sound Effects!(Sound when you send or receive a msg while in chat) - New: Call Metadata Notifier! diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json index 66b11e88..94c1640d 100644 --- a/common/src/main/assets/lang/en_US.json +++ b/common/src/main/assets/lang/en_US.json @@ -1300,10 +1300,6 @@ "name": "Call Metadata Notifier", "description": "Shows a notification with captured call metadata after the call ends" }, - "conversation_sound_effects": { - "name": "Conversation Sound Effects", - "description": "Plays send and receive sounds inside an open conversation" - }, "conversation_sound_effects_style": { "name": "Conversation Sound Style", "description": "Choose the sound style used for in-conversation send and receive sounds" @@ -2529,6 +2525,7 @@ "null": "Device default FPS" }, "conversation_sound_effects_style": { + "disabled": "Disabled", "imessage": "iMessage", "telegram": "Telegram", "whatsapp": "WhatsApp", diff --git a/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/MessagingTweaks.kt b/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/MessagingTweaks.kt index c88934bd..7cefc031 100644 --- a/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/MessagingTweaks.kt +++ b/common/src/main/kotlin/me/eternal/purrfectsnap/common/config/impl/MessagingTweaks.kt @@ -224,12 +224,12 @@ class MessagingTweaks : ConfigContainer() { val callStartConfirmation = boolean("call_start_confirmation") { requireRestart() } val blockCalls = boolean("block_calls") { requireRestart() } val callMetadataNotifier = boolean("call_metadata_notifier") { requireRestart(); addNotices(FeatureNotice.UNSTABLE) } - val conversationSoundEffects = boolean("conversation_sound_effects") { requireRestart() } - val conversationSoundEffectsStyle = unique("conversation_sound_effects_style", "imessage", "telegram", "whatsapp", "subtle") { + val conversationSoundEffectsStyle = unique("conversation_sound_effects_style", "disabled", "imessage", "telegram", "whatsapp", "subtle") { requireRestart() customOptionTranslationPath = "conversation_sound_effects_style" addFlags(ConfigFlag.NO_TRANSLATE) - }.apply { set("imessage") } + addFlags(ConfigFlag.NO_DISABLE_KEY) + }.apply { set("disabled") } val unlimitedConversationPinning = boolean("unlimited_conversation_pinning") { requireRestart() } val disableSnapModeRestrictions = boolean("disable_snap_mode_restrictions") { requireRestart() } val autoSaveMessagesInConversations = multiple("auto_save_messages_in_conversations", diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/downloader/MediaDownloader.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/downloader/MediaDownloader.kt index 486c4646..a30fba59 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/downloader/MediaDownloader.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/downloader/MediaDownloader.kt @@ -12,7 +12,6 @@ import android.widget.LinearLayout import android.widget.ProgressBar import android.widget.TextView import androidx.compose.foundation.background -import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Box @@ -30,14 +29,11 @@ import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Checkbox import androidx.compose.material3.CheckboxDefaults -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -45,8 +41,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -125,11 +119,6 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp private var lastSeenMediaInfoMap: MutableMap? = null var lastSeenMapParams: ParamMap? = null private set - private val storyPreviewCache = mutableMapOf>() - @Volatile - private var pendingBatchDownloadIndices: MutableList? = null - @Volatile - private var batchForceAllowDuplicate: Boolean = false private val translations by lazy { context.translation.getCategory("download_processor") } @@ -267,68 +256,10 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp val tr = context.translation.getCategory("download_processor.story_snap_dialog") val cancelStr = context.translation["button.cancel"] val downloadStr = context.translation["button.download"] - val previewCacheKey = buildString { - append(paramMap["STORY_ID"]?.toString() ?: "story") - append("|") - append(paramMap["TOPIC_SNAP_CREATOR_USER_ID"]?.toString() ?: "user") - append("|") - append(totalCount) - } context.runOnUiThread { createComposeAlertDialog(context.mainActivity!!) { alertDialog -> PurrfectOverlayTheme { val selected = remember { mutableStateListOf().apply { add(currentIndex) } } - val previewBitmaps = remember { mutableStateMapOf() } - val previewLoading = remember { mutableStateMapOf() } - - LaunchedEffect(Unit) { - if (!selected.contains(currentIndex)) selected.add(currentIndex) - mediaInfoMap[SplitMediaAssetType.ORIGINAL]?.uri?.let { currentUri -> - previewLoading[currentIndex] = true - previewBitmaps[currentIndex] = withContext(Dispatchers.IO) { loadStoryPreviewBitmap(currentUri) } - previewLoading[currentIndex] = false - } - synchronized(storyPreviewCache) { - storyPreviewCache[previewCacheKey]?.forEach { (index, bitmap) -> - previewBitmaps[index] = bitmap - } - } - } - - LaunchedEffect(previewCacheKey) { - val overlay = context.feature(OperaStoryOverlay::class) - val cachedIndices = synchronized(storyPreviewCache) { - storyPreviewCache.getOrPut(previewCacheKey) { mutableMapOf() }.keys.toSet() - } - val indicesToScan = (0 until totalCount).filter { it != currentIndex && it !in cachedIndices } - - try { - for (targetIndex in indicesToScan) { - val jumped = withContext(Dispatchers.Main) { - overlay.requestJumpToSnap(targetIndex, totalCount) - } - if (!jumped) continue - - val reached = waitForStoryIndex(targetIndex) - if (!reached) continue - - val uri = lastSeenMediaInfoMap?.get(SplitMediaAssetType.ORIGINAL)?.uri ?: continue - previewLoading[targetIndex] = true - val bitmap = withContext(Dispatchers.IO) { loadStoryPreviewBitmap(uri) } - previewLoading[targetIndex] = false - if (bitmap != null) { - previewBitmaps[targetIndex] = bitmap - synchronized(storyPreviewCache) { - storyPreviewCache.getOrPut(previewCacheKey) { mutableMapOf() }[targetIndex] = bitmap - } - } - } - } finally { - withContext(Dispatchers.Main) { - overlay.requestJumpToSnap(currentIndex, totalCount) - } - } - } PurrfectGlassCard( title = tr["title"], @@ -362,35 +293,6 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp }, colors = CheckboxDefaults.colors(checkedColor = PurrfectOverlayPalette.glowPrimary) ) - Box( - modifier = Modifier - .size(54.dp) - .background(Color.White.copy(alpha = 0.06f), RoundedCornerShape(12.dp)), - contentAlignment = Alignment.Center - ) { - val rowBitmap = previewBitmaps[index] - val rowLoading = previewLoading[index] == true - when { - rowBitmap != null -> Image( - bitmap = rowBitmap.asImageBitmap(), - contentDescription = null, - modifier = Modifier - .size(54.dp) - .background(Color.Transparent, RoundedCornerShape(12.dp)), - contentScale = ContentScale.Crop - ) - rowLoading -> CircularProgressIndicator( - color = PurrfectOverlayPalette.glowPrimary, - modifier = Modifier.size(22.dp), - strokeWidth = 2.dp - ) - else -> Icon( - imageVector = Icons.Outlined.Image, - contentDescription = null, - tint = PurrfectOverlayPalette.textSecondary - ) - } - } Text( label, style = MaterialTheme.typography.bodyMedium, @@ -435,10 +337,15 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp } Button( onClick = { - if (selected.isNotEmpty()) { - startBatchDownload(selected.sorted().toMutableList(), allowDuplicate) - alertDialog.dismiss() + if (!selected.contains(currentIndex)) return@Button + context.executeAsync { + runCatching { handleOperaMedia(paramMap, mediaInfoMap, true, allowDuplicate) } + .onFailure { + context.log.error("Story download failed", it) + context.shortToast(translations["failed_generic_toast"]) + } } + alertDialog.dismiss() }, modifier = Modifier.weight(1f), shape = RoundedCornerShape(14.dp), @@ -457,124 +364,6 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp } } - private suspend fun waitForStoryIndex(targetIndex: Int, timeoutMs: Long = 3000L): Boolean { - val startedAt = System.currentTimeMillis() - while (System.currentTimeMillis() - startedAt < timeoutMs) { - if (lastSeenMapParams?.getStorySnapIndex() == targetIndex) return true - kotlinx.coroutines.delay(60L) - } - return false - } - - private fun loadStoryPreviewBitmap(uriString: String): Bitmap? { - return runCatching { - val uri = Uri.parse(uriString) - when (uri.scheme?.lowercase()) { - "content" -> context.androidContext.contentResolver.openInputStream(uri)?.use(BitmapFactory::decodeStream) - "file", null -> BitmapFactory.decodeFile(uri.path) - "http", "https" -> { - runCatching { - OkHttpClient().newCall(Request.Builder().url(uriString).build()).execute().use { response -> - response.body?.byteStream()?.use { stream -> BitmapFactory.decodeStream(stream) } - } - }.getOrNull() ?: run { - val retriever = MediaMetadataRetriever() - try { - retriever.setDataSource(uriString, emptyMap()) - retriever.frameAtTime - } finally { - runCatching { retriever.release() } - } - } - } - else -> null - } ?: run { - val retriever = MediaMetadataRetriever() - try { - retriever.setDataSource(context.androidContext, uri) - retriever.frameAtTime - } finally { - runCatching { retriever.release() } - } - } - }.getOrNull() - } - - private fun startBatchDownload(indices: MutableList, allowDuplicate: Boolean) { - if (indices.isEmpty()) return - val paramMap = lastSeenMapParams ?: return - val mediaInfoMap = lastSeenMediaInfoMap ?: return - - pendingBatchDownloadIndices = indices - batchForceAllowDuplicate = allowDuplicate - - val currentIndex = paramMap.getStorySnapIndex() ?: 0 - val targetIndex = indices.first() - val totalCount = paramMap.getStorySnapTotal() - - if (currentIndex == targetIndex) { - processNextBatchDownload(paramMap, mediaInfoMap) - } else { - val jumped = context.feature(OperaStoryOverlay::class).requestJumpToSnap(targetIndex, totalCount) - if (!jumped) { - pendingBatchDownloadIndices = null - context.shortToast(translations["batch_download_jump_failed_toast"]) - } - } - } - - private fun downloadSingleSnap(paramMap: ParamMap, mediaInfoMap: Map) { - context.executeAsync { - runCatching { handleOperaMedia(paramMap, mediaInfoMap, true, batchForceAllowDuplicate) } - .onFailure { - context.log.error("Batch download failed", it) - context.shortToast(translations["failed_generic_toast"]) - } - } - } - - private fun processNextBatchDownload(paramMap: ParamMap, mediaInfoMap: Map) { - val queue = pendingBatchDownloadIndices ?: return - if (queue.isEmpty()) { - flushPendingMergeAndComplete() - return - } - - val currentIndex = paramMap.getStorySnapIndex() ?: -1 - if (currentIndex != queue.first()) return - - queue.removeAt(0) - downloadSingleSnap(paramMap, mediaInfoMap) - - if (queue.isEmpty()) { - flushPendingMergeAndComplete() - } else { - val totalCount = paramMap.getStorySnapTotal() - context.runOnUiThread { - fun tryJump(retryCount: Int = 0) { - val delayMs = if (retryCount == 0) 120L else 220L - android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ - val jumped = runCatching { - context.feature(OperaStoryOverlay::class).requestJumpToSnap(queue.first(), totalCount) - }.getOrNull() == true - if (!jumped && retryCount < 1) { - tryJump(retryCount + 1) - } else if (!jumped) { - pendingBatchDownloadIndices = null - context.shortToast(translations["batch_download_jump_failed_toast"]) - } - }, delayMs) - } - tryJump() - } - } - } - - private fun flushPendingMergeAndComplete() { - pendingBatchDownloadIndices = null - context.shortToast(translations["batch_download_complete_toast"]) - } - fun showLastOperaDebugMediaInfo() { if (lastSeenMapParams == null || lastSeenMediaInfoMap == null) return @@ -1063,15 +852,6 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp lastSeenMapParams = mediaParamMap lastSeenMediaInfoMap = mediaInfoMap - if (pendingBatchDownloadIndices != null) { - android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ - if (pendingBatchDownloadIndices != null) { - processNextBatchDownload(mediaParamMap, mediaInfoMap) - } - }, 80L) - return@onOperaViewStateCallback - } - if (!shouldAutoDownload) { return@onOperaViewStateCallback } diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/ConversationSoundEffects.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/ConversationSoundEffects.kt index 74c93b31..db240292 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/ConversationSoundEffects.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/ConversationSoundEffects.kt @@ -1,87 +1,244 @@ package me.eternal.purrfectsnap.core.features.impl.messaging +import android.media.AudioAttributes +import android.media.AudioFormat import android.media.AudioManager -import android.media.ToneGenerator +import android.media.AudioTrack import kotlinx.coroutines.delay import me.eternal.purrfectsnap.core.event.events.impl.ConversationUpdateEvent import me.eternal.purrfectsnap.core.event.events.impl.SendMessageWithContentEvent import me.eternal.purrfectsnap.core.features.Feature +import kotlin.math.PI +import kotlin.math.exp +import kotlin.math.sin class ConversationSoundEffects : Feature("Conversation Sound Effects") { private val seenIncomingMessageIds = LinkedHashSet() private val maxTrackedMessages = 512 - private data class ToneStep( - val tone: Int, + private data class BubbleSpec( val durationMs: Int, + val startFreqHz: Double, + val endFreqHz: Double, + val overtoneFreqHz: Double, + val amplitude: Double + ) + + private data class BubbleStep( + val spec: BubbleSpec, val pauseAfterMs: Long = 0L ) - private data class ToneSpec( - val sendPattern: List, - val receivePattern: List + private val iMessageSendBubble = BubbleSpec( + durationMs = 78, + startFreqHz = 1160.0, + endFreqHz = 690.0, + overtoneFreqHz = 1820.0, + amplitude = 0.50 + ) + + private val iMessageReceiveBubble = BubbleSpec( + durationMs = 92, + startFreqHz = 1040.0, + endFreqHz = 640.0, + overtoneFreqHz = 1680.0, + amplitude = 0.46 + ) + + private val whatsappSendBubble = BubbleSpec( + durationMs = 86, + startFreqHz = 860.0, + endFreqHz = 520.0, + overtoneFreqHz = 1410.0, + amplitude = 0.52 + ) + + private val whatsappReceiveBubble = BubbleSpec( + durationMs = 94, + startFreqHz = 920.0, + endFreqHz = 560.0, + overtoneFreqHz = 1520.0, + amplitude = 0.50 + ) + + private val telegramSendPrimary = BubbleSpec( + durationMs = 58, + startFreqHz = 1110.0, + endFreqHz = 820.0, + overtoneFreqHz = 1710.0, + amplitude = 0.42 + ) + + private val telegramSendAccent = BubbleSpec( + durationMs = 34, + startFreqHz = 1360.0, + endFreqHz = 980.0, + overtoneFreqHz = 2060.0, + amplitude = 0.22 + ) + + private val telegramReceivePrimary = BubbleSpec( + durationMs = 72, + startFreqHz = 1080.0, + endFreqHz = 780.0, + overtoneFreqHz = 1680.0, + amplitude = 0.44 + ) + + private val telegramReceiveAccent = BubbleSpec( + durationMs = 42, + startFreqHz = 1280.0, + endFreqHz = 940.0, + overtoneFreqHz = 1940.0, + amplitude = 0.18 + ) + + private val subtleSendBubble = BubbleSpec( + durationMs = 60, + startFreqHz = 760.0, + endFreqHz = 520.0, + overtoneFreqHz = 1180.0, + amplitude = 0.26 + ) + + private val subtleReceiveBubble = BubbleSpec( + durationMs = 66, + startFreqHz = 800.0, + endFreqHz = 560.0, + overtoneFreqHz = 1260.0, + amplitude = 0.24 ) private fun currentConversationId() = context.feature(Messaging::class).openedConversationUUID?.toString() - private fun styleSpec(): ToneSpec { - return when (context.config.messaging.conversationSoundEffectsStyle.get()) { - "telegram" -> ToneSpec( - sendPattern = listOf( - ToneStep(ToneGenerator.TONE_PROP_BEEP, 35), - ToneStep(ToneGenerator.TONE_PROP_BEEP2, 45, 25) - ), - receivePattern = listOf( - ToneStep(ToneGenerator.TONE_CDMA_ALERT_CALL_GUARD, 70), - ToneStep(ToneGenerator.TONE_PROP_BEEP2, 35, 20) - ) - ) - "whatsapp" -> ToneSpec( - sendPattern = listOf( - ToneStep(ToneGenerator.TONE_PROP_ACK, 55) - ), - receivePattern = listOf( - ToneStep(ToneGenerator.TONE_CDMA_ALERT_CALL_GUARD, 85), - ToneStep(ToneGenerator.TONE_PROP_ACK, 35, 15) - ) - ) - "subtle" -> ToneSpec( - sendPattern = listOf( - ToneStep(ToneGenerator.TONE_PROP_PROMPT, 22) - ), - receivePattern = listOf( - ToneStep(ToneGenerator.TONE_PROP_ACK, 28) - ) - ) - else -> ToneSpec( - sendPattern = listOf( - ToneStep(ToneGenerator.TONE_PROP_PROMPT, 40), - ToneStep(ToneGenerator.TONE_PROP_BEEP, 28, 18) - ), - receivePattern = listOf( - ToneStep(ToneGenerator.TONE_PROP_ACK, 55), - ToneStep(ToneGenerator.TONE_PROP_BEEP2, 40, 22) - ) + private fun buildBubblePcm(spec: BubbleSpec, sampleRate: Int = 44_100): ByteArray { + val sampleCount = (sampleRate * (spec.durationMs / 1000.0)).toInt().coerceAtLeast(1) + val pcm = ByteArray(sampleCount * 2) + for (i in 0 until sampleCount) { + val progress = i.toDouble() / sampleCount.toDouble() + val envelope = exp(-4.8 * progress) * (1.0 - exp(-20.0 * progress)) + val freq = spec.startFreqHz + (spec.endFreqHz - spec.startFreqHz) * progress + val t = i.toDouble() / sampleRate.toDouble() + val fundamental = sin(2.0 * PI * freq * t) + val overtone = 0.18 * sin(2.0 * PI * spec.overtoneFreqHz * t) + val airyTail = 0.08 * sin(2.0 * PI * (freq * 0.48) * t) + val warmth = 0.14 * sin(2.0 * PI * (freq * 0.24) * t) + val sample = ((fundamental + overtone + airyTail + warmth) * envelope * spec.amplitude) + .coerceIn(-1.0, 1.0) + val shortValue = (sample * Short.MAX_VALUE).toInt().toShort() + pcm[i * 2] = (shortValue.toInt() and 0xFF).toByte() + pcm[i * 2 + 1] = ((shortValue.toInt() shr 8) and 0xFF).toByte() + } + return pcm + } + + private fun playBubble(spec: BubbleSpec) { + if (context.isMainActivityPaused) return + context.executeAsync { + val sampleRate = 44_100 + val pcm = buildBubblePcm(spec, sampleRate) + val audioTrack = AudioTrack( + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_NOTIFICATION_EVENT) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build(), + AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_PCM_16BIT) + .setSampleRate(sampleRate) + .setChannelMask(AudioFormat.CHANNEL_OUT_MONO) + .build(), + pcm.size, + AudioTrack.MODE_STATIC, + AudioManager.AUDIO_SESSION_ID_GENERATE ) + runCatching { + audioTrack.write(pcm, 0, pcm.size) + audioTrack.play() + delay(spec.durationMs.toLong() + 24L) + }.also { + runCatching { + audioTrack.stop() + audioTrack.release() + } + } } } - private fun playPattern(pattern: List) { + private fun playBubbleSequence(steps: List) { if (context.isMainActivityPaused) return context.executeAsync { - var toneGenerator: ToneGenerator? = null - runCatching { - toneGenerator = ToneGenerator(AudioManager.STREAM_NOTIFICATION, 55) - pattern.forEach { step -> - toneGenerator?.startTone(step.tone, step.durationMs) - if (step.pauseAfterMs > 0) delay(step.pauseAfterMs) + steps.forEach { step -> + val sampleRate = 44_100 + val pcm = buildBubblePcm(step.spec, sampleRate) + val audioTrack = AudioTrack( + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_NOTIFICATION_EVENT) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build(), + AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_PCM_16BIT) + .setSampleRate(sampleRate) + .setChannelMask(AudioFormat.CHANNEL_OUT_MONO) + .build(), + pcm.size, + AudioTrack.MODE_STATIC, + AudioManager.AUDIO_SESSION_ID_GENERATE + ) + runCatching { + audioTrack.write(pcm, 0, pcm.size) + audioTrack.play() + delay(step.spec.durationMs.toLong() + step.pauseAfterMs + 18L) + }.also { + runCatching { + audioTrack.stop() + audioTrack.release() + } } - }.also { - runCatching { toneGenerator?.release() } } } } + private fun playStyledSend() { + when (context.config.messaging.conversationSoundEffectsStyle.get()) { + "imessage" -> playBubble(iMessageSendBubble) + "whatsapp" -> playBubble(whatsappSendBubble) + "telegram" -> playBubbleSequence( + listOf( + BubbleStep(telegramSendPrimary, pauseAfterMs = 16L), + BubbleStep(telegramSendAccent) + ) + ) + else -> playBubble(subtleSendBubble) + } + } + + private fun playStyledReceive() { + when (context.config.messaging.conversationSoundEffectsStyle.get()) { + "imessage" -> playBubble(iMessageReceiveBubble) + "whatsapp" -> playBubbleSequence( + listOf( + BubbleStep(whatsappReceiveBubble, pauseAfterMs = 12L), + BubbleStep( + whatsappReceiveBubble.copy( + durationMs = 42, + startFreqHz = 1210.0, + endFreqHz = 860.0, + overtoneFreqHz = 1980.0, + amplitude = 0.20 + ) + ) + ) + ) + "telegram" -> playBubbleSequence( + listOf( + BubbleStep(telegramReceivePrimary, pauseAfterMs = 14L), + BubbleStep(telegramReceiveAccent) + ) + ) + else -> playBubble(subtleReceiveBubble) + } + } + private fun markSeen(messageId: Long): Boolean { synchronized(seenIncomingMessageIds) { val added = seenIncomingMessageIds.add(messageId) @@ -93,15 +250,14 @@ class ConversationSoundEffects : Feature("Conversation Sound Effects") { } override fun init() { - if (!context.config.messaging.conversationSoundEffects.get()) return + if (context.config.messaging.conversationSoundEffectsStyle.get() == "disabled") return context.event.subscribe(SendMessageWithContentEvent::class) { event -> val activeConversationId = currentConversationId() ?: return@subscribe if (event.destinations.conversations?.none { it.toString() == activeConversationId } != false) return@subscribe event.addCallbackResult("onSuccess") { - val spec = styleSpec() - playPattern(spec.sendPattern) + playStyledSend() } } @@ -110,7 +266,6 @@ class ConversationSoundEffects : Feature("Conversation Sound Effects") { if (event.conversationId != activeConversationId) return@subscribe val myUserId = context.database.myUserId ?: return@subscribe - val spec = styleSpec() event.messages .asSequence() @@ -119,7 +274,7 @@ class ConversationSoundEffects : Feature("Conversation Sound Effects") { .filter { markSeen(it) } .firstOrNull() ?.let { - playPattern(spec.receivePattern) + playStyledReceive() } } } diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/tweaks/CameraTweaks.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/tweaks/CameraTweaks.kt index 3059d48f..947b3999 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/tweaks/CameraTweaks.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/tweaks/CameraTweaks.kt @@ -121,11 +121,6 @@ class CameraTweaks : Feature("Camera Tweaks") { param.setArg(1, captureResolutionConfig[1]) } - CaptureRequest.Builder::class.java.hook("set", HookStage.BEFORE) { param -> - val key = param.arg>(0) - if (key == CaptureRequest.CONTROL_ZOOM_RATIO) return@hook - } - CameraCharacteristics::class.java.hook("get", HookStage.AFTER) { param -> val key = param.argNullable>(0) ?: return@hook @@ -151,6 +146,14 @@ class CameraTweaks : Feature("Camera Tweaks") { } } } + + if (key == CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES) { + val isFrontCamera = param.invokeOriginal( + arrayOf(CameraCharacteristics.LENS_FACING) + ) == CameraCharacteristics.LENS_FACING_FRONT + val customFrameRate = (if (isFrontCamera) config.frontCustomFrameRate.getNullable() else config.backCustomFrameRate.getNullable())?.toIntOrNull() ?: return@hook + param.setResult(arrayOf(Range(customFrameRate, customFrameRate))) + } } if (config.blackPhotos.get()) { diff --git a/gradle.properties b/gradle.properties index 6e169db2..41acf880 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,8 +7,8 @@ org.gradle.configuration-cache=true org.gradle.configuration-cache.problems=warn nativeAbis=arm64-v8a -APP_VERSION_NAME=1.6.0 -APP_VERSION_CODE=310 +APP_VERSION_NAME=1.6.1 +APP_VERSION_CODE=312 debug_build_hash=18fe2a814d0e2eb5 psIntegrityPinnedSha256= EXPECTED_CERT_SHA256=0f188cb17d8ea4902ee15ce98f4928ba4226a4790fa3c52f62d776b08e46ca1c