Skip to content

Commit ebefcc4

Browse files
Sirpixelalotclaude
andcommitted
Add comprehensive game compression system with image, audio, and video support
Features: - Image compression using native Android Bitmap APIs with WebP format (lossless/lossy) - Audio compression using FFmpeg-Kit with Opus codec - Video compression using FFmpeg-Kit with h264_mediacodec (hardware acceleration) - Compression settings dialog with quality controls for all media types - Thread control for parallel image processing - Pre-scan to calculate total original size before compression - Custom compression success dialog showing size reduction statistics - Phase-specific progress indicators (Compressing Images/Audio/Video) - Visible scrollbar in compression settings dialog - Support for transparent images (RGBA preserved in WebP) Implementation: - Created ImageCompressor.kt for native Kotlin image compression with parallel processing - Created AudioCompressor.kt using FFmpeg-Kit with Opus encoding - Created VideoCompressor.kt using FFmpeg-Kit with MediaCodec H.264 encoding - Created CompressionManager.kt to orchestrate all compression phases - Created CompressionSettings.kt for persistent user preferences - Created CompressionSettingsDialog.kt with Material 3 UI - Added FFmpeg-Kit AAR library (version 6.0-2.LTS) for audio/video processing - Integrated compression into MainActivity with settings persistence - Defaults to same-folder compression (overwrites originals) Bug Fixes: - Fixed batch extraction/creation progress counter not updating during operation - Modified Python rpa_wrapper.py to accept and preserve batch information - Updated MainViewModel to pass batch info (index, total, filename) to Python - Python now includes batch info in all progress updates - ProgressViewModel simplified to read batch index directly from ProgressData 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 2958119 commit ebefcc4

21 files changed

+2239
-45
lines changed

app/build.gradle

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@ android {
3030
}
3131

3232
python {
33-
// Chaquopy will auto-detect Python installation
34-
// If needed, uncomment and set your local Python path:
35-
// buildPython "C:/Python314/python.exe"
33+
// Use Python 3.8 for best Chaquopy compatibility
34+
version "3.8"
35+
36+
// No longer need Pillow - using native Android Bitmap APIs for image compression
3637
}
3738
}
3839

@@ -108,6 +109,11 @@ dependencies {
108109
implementation 'io.github.rosemoe:editor'
109110
implementation 'io.github.rosemoe:language-textmate'
110111

112+
// FFmpeg-Kit for audio/video compression (local AAR)
113+
implementation files('libs/ffmpeg-kit-full-6.0-2.LTS.aar')
114+
// FFmpeg-Kit dependencies
115+
implementation 'com.arthenica:smart-exception-java:0.2.1'
116+
111117
// Testing
112118
testImplementation libs.junit
113119
androidTestImplementation libs.ext.junit
62.5 MB
Binary file not shown.
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package com.renpytool
2+
3+
import android.util.Log
4+
import com.arthenica.ffmpegkit.FFmpegKit
5+
import com.arthenica.ffmpegkit.ReturnCode
6+
import kotlinx.coroutines.Dispatchers
7+
import kotlinx.coroutines.withContext
8+
import java.io.File
9+
10+
/**
11+
* Handles audio compression using FFmpeg-Kit
12+
* Audio-only compression to Opus codec
13+
*/
14+
object AudioCompressor {
15+
16+
private const val TAG = "AudioCompressor"
17+
18+
// Audio quality presets (bitrates)
19+
enum class AudioQuality(val bitrate: String) {
20+
HIGH("128k"),
21+
MEDIUM("48k"),
22+
LOW("24k"),
23+
BAD("16k")
24+
}
25+
26+
data class CompressResult(
27+
val success: Boolean,
28+
val originalSize: Long = 0,
29+
val compressedSize: Long = 0,
30+
val errorMessage: String? = null
31+
)
32+
33+
/**
34+
* Scan directory for audio files
35+
*/
36+
fun scanAudioFiles(directory: File): List<File> {
37+
val audioExtensions = setOf("ogg", "mp3", "wav", "flac", "m4a", "aac", "opus")
38+
val files = mutableListOf<File>()
39+
40+
Log.i(TAG, "Scanning for audio files in: ${directory.absolutePath}")
41+
42+
try {
43+
directory.walkTopDown()
44+
.filter { it.isFile }
45+
.filter { it.extension.lowercase() in audioExtensions }
46+
.forEach { file ->
47+
files.add(file)
48+
}
49+
} catch (e: Exception) {
50+
Log.e(TAG, "Error scanning audio files", e)
51+
}
52+
53+
Log.i(TAG, "Found ${files.size} audio files")
54+
return files
55+
}
56+
57+
/**
58+
* Compress audio file to Opus codec in OGG container
59+
*/
60+
suspend fun compressAudio(
61+
inputFile: File,
62+
outputFile: File,
63+
quality: AudioQuality,
64+
threads: Int? = null
65+
): CompressResult = withContext(Dispatchers.IO) {
66+
try {
67+
if (!inputFile.exists()) {
68+
return@withContext CompressResult(false, errorMessage = "Input file does not exist")
69+
}
70+
71+
val originalSize = inputFile.length()
72+
73+
// Create output directory if needed
74+
outputFile.parentFile?.mkdirs()
75+
76+
// Temporary output file with .temp.ogg extension
77+
val tempOutput = File(outputFile.parentFile, "${outputFile.nameWithoutExtension}.temp.ogg")
78+
if (tempOutput.exists()) tempOutput.delete()
79+
80+
// Build FFmpeg command for Opus audio compression
81+
val threadArg = threads?.let { "-threads ${it}" } ?: ""
82+
val command = "-i \"${inputFile.absolutePath}\" -c:a libopus -b:a ${quality.bitrate} -vbr on $threadArg -y \"${tempOutput.absolutePath}\""
83+
84+
Log.i(TAG, "Compressing audio: ${inputFile.name} to Opus ${quality.bitrate}")
85+
86+
// Execute FFmpeg command
87+
val session = FFmpegKit.execute(command)
88+
val returnCode = session.returnCode
89+
90+
if (ReturnCode.isSuccess(returnCode)) {
91+
// Rename temp file to final output with original extension preserved
92+
val finalOutput = File(outputFile.parentFile, outputFile.name)
93+
if (finalOutput.exists()) finalOutput.delete()
94+
tempOutput.renameTo(finalOutput)
95+
96+
val compressedSize = finalOutput.length()
97+
Log.i(TAG, "Audio compression succeeded: ${inputFile.name} (${originalSize}b -> ${compressedSize}b)")
98+
99+
CompressResult(
100+
success = true,
101+
originalSize = originalSize,
102+
compressedSize = compressedSize
103+
)
104+
} else {
105+
val error = "FFmpeg returned code ${returnCode.value}"
106+
Log.e(TAG, "Audio compression failed: $error")
107+
if (tempOutput.exists()) tempOutput.delete()
108+
CompressResult(false, errorMessage = error)
109+
}
110+
} catch (e: Exception) {
111+
Log.e(TAG, "Audio compression exception", e)
112+
CompressResult(false, errorMessage = e.message)
113+
}
114+
}
115+
}

0 commit comments

Comments
 (0)