Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions .github/workflows/build_apk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
name: Build APK

on:
push:
branches: [main]
tags: ["v*"]
pull_request:
branches: [main]
workflow_dispatch:

concurrency:
group: build-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
name: Build & Sign APK
runs-on: ubuntu-latest
env:
HAS_KEYSTORE: ${{ secrets.KEYSTORE_BASE64 != '' }}

steps:
- name: Checkout
uses: actions/checkout@v7

- name: Set up JDK 21
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
cache: gradle

- name: Grant Gradle execute permission
run: chmod +x gradlew

# ── Signing setup ──────────────────────────────────────────────────────
# Runs only when the required secrets are present (skipped on forks/PRs
# without access to secrets).
- name: Decode keystore
if: env.HAS_KEYSTORE == 'true'
run: |
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > keystore.jks

- name: Write local.properties (signed)
if: env.HAS_KEYSTORE == 'true'
run: |
cat >> local.properties <<EOF
KEYSTORE_FILE=${{ github.workspace }}/keystore.jks
KEYSTORE_PASSWORD=${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS=${{ secrets.KEY_ALIAS }}
KEY_PASSWORD=${{ secrets.KEY_PASSWORD }}
EOF

# ── Build ──────────────────────────────────────────────────────────────
- name: Build debug APK
run: ./gradlew assembleDebug --no-daemon --build-cache

- name: Build release APK
run: ./gradlew assembleRelease --no-daemon --build-cache

# ── Resolve version for artifact naming ───────────────────────────────
- name: Extract version name
id: version
run: |
VERSION=$(grep 'versionName' app/build.gradle.kts | awk -F'"' '{print $2}')
echo "name=$VERSION" >> "$GITHUB_OUTPUT"

# ── Rename outputs ────────────────────────────────────────────────────
- name: Rename APKs
run: |
mkdir -p artifacts
# Debug
cp app/build/outputs/apk/debug/app-debug.apk \
artifacts/ViPER4Android-${{ steps.version.outputs.name }}-debug.apk
# Release — signed or unsigned depending on secrets availability
RELEASE_APK=$(ls app/build/outputs/apk/release/app-release*.apk | head -1)
cp "$RELEASE_APK" \
artifacts/ViPER4Android-${{ steps.version.outputs.name }}.apk

# ── Upload artifacts (every build) ────────────────────────────────────
- name: Upload debug APK
uses: actions/upload-artifact@v7
with:
name: debug-apk
path: artifacts/ViPER4Android-${{ steps.version.outputs.name }}-debug.apk
retention-days: 7

- name: Upload release APK
uses: actions/upload-artifact@v7
with:
name: release-apk
path: artifacts/ViPER4Android-${{ steps.version.outputs.name }}.apk
retention-days: 30

# ── GitHub Release (tag pushes only) ──────────────────────────────────
- name: Create GitHub Release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v3
with:
files: |
artifacts/ViPER4Android-${{ steps.version.outputs.name }}.apk
artifacts/ViPER4Android-${{ steps.version.outputs.name }}-debug.apk
generate_release_notes: true
12 changes: 9 additions & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,26 @@ android {
}

val keystoreFile = localProps.getProperty("KEYSTORE_FILE", "")
if (keystoreFile.isNotEmpty()) {
signingConfigs {
signingConfigs {
if (keystoreFile.isNotEmpty()) {
create("release") {
storeFile = file(keystoreFile)
storePassword = localProps.getProperty("KEYSTORE_PASSWORD", "")
keyAlias = localProps.getProperty("KEY_ALIAS", "")
keyPassword = localProps.getProperty("KEY_PASSWORD", "")
}
}
// "debug" signing config is provided automatically by AGP; no declaration needed.
}

buildTypes {
release {
signingConfig = if (keystoreFile.isNotEmpty()) signingConfigs.getByName("release") else null
// Use production keystore when available, otherwise fall back to the
// debug keystore so the APK is always installable (sideload / CI).
signingConfig = if (keystoreFile.isNotEmpty())
signingConfigs.getByName("release")
else
signingConfigs.getByName("debug")
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
Expand Down
24 changes: 24 additions & 0 deletions app/src/main/java/com/llsl/viper4android/effect/EffectGroups.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1241,6 +1241,30 @@ class TubeSimulatorEffect : EffectGroupBuilder("tubeSimulator") {
{ it.tubeSimulator.enable },
{ copy(tubeSimulator = tubeSimulator.copy(enable = it)) },
)
val model =
int(
ViperParams.PARAM_TUBE_SIMULATOR_MODEL,
"model",
0,
{ it.tubeSimulator.model },
{ copy(tubeSimulator = tubeSimulator.copy(model = it)) },
)
val drive =
int(
ViperParams.PARAM_TUBE_SIMULATOR_DRIVE,
"drive",
200,
{ it.tubeSimulator.drive },
{ copy(tubeSimulator = tubeSimulator.copy(drive = it)) },
)
val mix =
int(
ViperParams.PARAM_TUBE_SIMULATOR_MIX,
"mix",
30,
{ it.tubeSimulator.mix },
{ copy(tubeSimulator = tubeSimulator.copy(mix = it)) },
)
}

class AnalogXEffect : EffectGroupBuilder("analogX") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,9 @@ data class AnalogXState(

data class TubeSimulatorState(
val enable: Boolean = false,
val model: Int = 0, // 0 = 12AX7, 1 = 6N1J
val drive: Int = 200,
val mix: Int = 30,
)

data class SpeakerCorrectionState(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2015,15 +2015,56 @@ fun TubeSimulatorSection(
) {
val vals = state.tubeSimulator
val enabled = vals.enable
val model = vals.model
val drive = vals.drive
val mix = vals.mix

val tubeNames = listOf("12AX7", "6N1J")

EffectSection(
title = stringResource(R.string.section_tube_simulator),
enabled = enabled,
onEnabledChange = viewModel::setTubeSimulatorEnabled,
descriptionRes = R.string.effect_desc_tube_simulator,
icon = Icons.Default.MusicNote,
toggleOnly = true,
) {}
) {
LabeledDropdown(
label = stringResource(R.string.label_tube_type),
selectedValue = tubeNames.getOrElse(model) { tubeNames[0] },
options = tubeNames,
onOptionSelected = { index, _ -> viewModel.applyPref(Effects.tubeSimulator.model, index) },
)
LabeledSlider(
label = stringResource(R.string.label_tube_drive),
value = drive.toFloat(),
onValueChange = { viewModel.applyPref(Effects.tubeSimulator.drive, it.roundToInt()) },
valueRange = 100f..1000f,
valueLabel = "${"%.1f".format(drive / 100.0)}",
edit =
SliderEdit(
displayValue = drive / 100.0,
displayRange = 1.0..10.0,
decimals = 1,
unit = "",
onCommit = { viewModel.applyPref(Effects.tubeSimulator.drive, (it * 100).roundToInt().coerceIn(100, 1000)) },
),
)
LabeledSlider(
label = stringResource(R.string.label_tube_mix),
value = mix.toFloat(),
onValueChange = { viewModel.applyPref(Effects.tubeSimulator.mix, it.roundToInt()) },
valueRange = 0f..100f,
valueLabel = "$mix%",
edit =
SliderEdit(
displayValue = mix.toDouble(),
displayRange = 0.0..100.0,
decimals = 0,
unit = "%",
onCommit = { viewModel.applyPref(Effects.tubeSimulator.mix, it.roundToInt().coerceIn(0, 100)) },
),
)
}
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -843,7 +843,13 @@ class MainViewModel
}

fun setTubeSimulatorEnabled(enabled: Boolean) {
applyPref(Effects.tubeSimulator.enable, enabled)
applyPref(Effects.tubeSimulator.enable, enabled, last = !enabled)
if (enabled) {
val v = uiState.value.tubeSimulator
applyPref(Effects.tubeSimulator.model, v.model, last = false)
applyPref(Effects.tubeSimulator.drive, v.drive, last = false)
applyPref(Effects.tubeSimulator.mix, v.mix)
}
}

fun setPsychoacousticBassEnabled(enabled: Boolean) {
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/java/com/llsl/viper4android/viper/ViperParams.kt
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ object ViperParams {
const val PARAM_CURE_CROSSFEED_PRESET = 0x10241

const val PARAM_TUBE_SIMULATOR_ENABLE = 0x10250
const val PARAM_TUBE_SIMULATOR_MODEL = 0x10251
const val PARAM_TUBE_SIMULATOR_DRIVE = 0x10252
const val PARAM_TUBE_SIMULATOR_MIX = 0x10253
const val PARAM_ANALOG_X_ENABLE = 0x10260
const val PARAM_ANALOG_X_MODE = 0x10261

Expand Down
5 changes: 4 additions & 1 deletion app/src/main/res/values-ru/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,10 @@
<string name="label_dynamic_system_side_gain_high">Усиление боковых каналов для высоких частот</string>

<!-- Tube Simulator -->
<string name="section_tube_simulator">Ламповый усилитель (6N1J)</string>
<string name="section_tube_simulator">Симулятор лампы</string>
<string name="label_tube_type">Тип лампы</string>
<string name="label_tube_drive">Усиление</string>
<string name="label_tube_mix">Смешение</string>

<!-- Psychoacoustic Bass -->
<string name="section_psycho_bass">Психоакустический Bass</string>
Expand Down
Loading
Loading