10 Commits

Author SHA1 Message Date
pi 0c05ed1c55 added zombi_refrain 2026-04-13 23:24:18 +02:00
pi 2125cefdac added versions number in app/build.gradle.kts 2026-04-13 23:22:19 +02:00
pi 29be5a0e74 tweaked zombi and added refrain only 2026-04-13 23:12:16 +02:00
pi 64e491805c added dwarfs and removed filenameextension 2026-04-13 12:44:13 +02:00
pi e12950c25c neue musik 2026-04-12 10:25:20 +02:00
pi 7552ac1942 neue musik 2026-04-11 13:14:00 +02:00
pi 282837d415 hairdryer is always #1 2026-04-11 12:49:28 +02:00
pi 0b1f1a2121 changed from playsound to exoplayer2 because of 5,6seconds limit 2026-04-11 12:48:30 +02:00
pi 10c6a8eb95 farben geändert, default werte fade on und 20 minuten 2025-12-06 22:44:14 +01:00
pi 82276868c4 drehen und weiterspielen funktioniert jetzt
fixes #1
closes #1
resolves #1
2025-11-29 16:23:27 +01:00
15 changed files with 180 additions and 39 deletions
+8
View File
@@ -4,6 +4,14 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-01-24T22:53:27.667021Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=000988cf644b8f" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState> </SelectionState>
</selectionStates> </selectionStates>
</component> </component>
+3 -2
View File
@@ -15,7 +15,7 @@ android {
minSdk = 22 minSdk = 22
targetSdk = 36 targetSdk = 36
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "0.10.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -57,5 +57,6 @@ dependencies {
androidTestImplementation(libs.androidx.compose.ui.test.junit4) androidTestImplementation(libs.androidx.compose.ui.test.junit4)
debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest) debugImplementation(libs.androidx.compose.ui.test.manifest)
implementation("androidx.appcompat:appcompat:1.7.0") implementation("androidx.appcompat:appcompat:1.7.1")
implementation("com.google.android.exoplayer:exoplayer:2.19.1")
} }
+3 -2
View File
@@ -8,9 +8,10 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@android:style/Theme.Light.NoTitleBar"> android:theme="@android:style/Theme.Light.NoTitleBar">
<activity android:name=".MainActivity" <activity android:name=".MainActivity"
android:exported="true"> android:exported="true"
android:configChanges="orientation|screenSize|keyboardHidden"
>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
@@ -2,51 +2,88 @@ package com.example.hairdryer
import android.app.Activity import android.app.Activity
import android.graphics.Color import android.graphics.Color
import android.media.AudioAttributes //import android.os.Build
import android.media.SoundPool
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.widget.EditText import android.widget.*
import android.widget.ProgressBar import android.content.pm.PackageManager
import android.widget.TextView import java.io.File
import kotlin.math.pow
import kotlin.math.max import kotlin.math.max
import kotlin.math.pow
// 🔥 ExoPlayer Imports
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.Player
class MainActivity : Activity() { class MainActivity : Activity() {
private lateinit var soundPool: SoundPool private var player: ExoPlayer? = null
private var soundId = 0
private var streamId = 0
private val handler = Handler() private val handler = Handler()
private var isPlaying = false private var isPlaying = false
private var fadeEnabled = false private var fadeEnabled = true
private var totalMillis: Long = 0 private var totalMillis: Long = 0
private var startTime: Long = 0 private var startTime: Long = 0
private var fadeRunnable: Runnable? = null private var fadeRunnable: Runnable? = null
private var countdownRunnable: Runnable? = null private var countdownRunnable: Runnable? = null
private var audioFiles: List<File> = emptyList()
private var selectedFileIndex = 0
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
// if (!checkPermissions()) return
val soundFolder = File(getExternalFilesDir(null), "babysounds")
if (!soundFolder.exists()) soundFolder.mkdirs()
cleanOldSounds(soundFolder, keepFiles = listOf("hairdryer.ogg", "fezforgotten.ogg"))
copyRawToFiles(R.raw.hairdryer, "hairdryer.ogg", soundFolder)
copyRawToFiles(R.raw.fezforgotten, "fezforgotten.ogg", soundFolder)
copyRawToFiles(R.raw.feuer, "feuer.ogg", soundFolder)
copyRawToFiles(R.raw.dwarfs, "dwarfs.ogg", soundFolder)
copyRawToFiles(R.raw.meeresrauschen, "meeresrauschen.ogg", soundFolder)
copyRawToFiles(R.raw.regen, "regen.ogg", soundFolder)
copyRawToFiles(R.raw.zombi, "zombi.ogg", soundFolder)
copyRawToFiles(R.raw.zombi_refrain, "zombi-refrain.ogg", soundFolder)
val status = findViewById<TextView>(R.id.statusText) val status = findViewById<TextView>(R.id.statusText)
val input = findViewById<EditText>(R.id.timerInput) val input = findViewById<EditText>(R.id.timerInput)
val fadeToggle = findViewById<TextView>(R.id.fadeToggle) val fadeToggle = findViewById<TextView>(R.id.fadeToggle)
val volumeBar = findViewById<ProgressBar>(R.id.volumeBar) val volumeBar = findViewById<ProgressBar>(R.id.volumeBar)
val spinner = findViewById<Spinner>(R.id.mp3Spinner)
val attrs = AudioAttributes.Builder() audioFiles = getAudioFilesFromFolder(soundFolder)
.setUsage(AudioAttributes.USAGE_MEDIA) .sortedWith(compareByDescending<File> { it.name == "hairdryer.ogg" }
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .thenBy { it.name })
.build()
soundPool = SoundPool.Builder() val names = audioFiles.map { it.nameWithoutExtension }
.setMaxStreams(1)
.setAudioAttributes(attrs)
.build()
soundId = soundPool.load(this, R.raw.sound, 1) val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, names)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinner.adapter = adapter
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: android.view.View?,
position: Int,
id: Long
) {
selectedFileIndex = position
stopSound(status, volumeBar)
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
updateUI(status, volumeBar, "Bereit", Color.GREEN, 1f)
updateUI(status, volumeBar, "Bereit", Color.LTGRAY, 1f)
fadeToggle.setOnClickListener { fadeToggle.setOnClickListener {
fadeEnabled = !fadeEnabled fadeEnabled = !fadeEnabled
fadeToggle.text = "Fade-Out: " + if (fadeEnabled) "ON" else "OFF" fadeToggle.text = "Fade-Out: " + if (fadeEnabled) "ON" else "OFF"
@@ -62,21 +99,97 @@ class MainActivity : Activity() {
} }
} }
// --- RAW -> files ---
private fun copyRawToFiles(rawId: Int, fileName: String, folder: File) {
if (!folder.exists()) folder.mkdirs()
val outFile = File(folder, fileName)
if (!outFile.exists()) {
resources.openRawResource(rawId).use { input ->
outFile.outputStream().use { output ->
input.copyTo(output)
}
}
}
}
// --- Alte Sounds löschen ---
private fun cleanOldSounds(folder: File, keepFiles: List<String>) {
folder.listFiles { file ->
file.isFile &&
(file.extension.equals("mp3", true) || file.extension.equals("ogg", true)) &&
file.name !in keepFiles
}?.forEach { it.delete() }
}
// --- Audio-Dateien holen ---
private fun getAudioFilesFromFolder(folder: File): List<File> {
return folder.listFiles { file ->
file.isFile &&
(file.extension.equals("mp3", true) || file.extension.equals("ogg", true))
}?.toList() ?: emptyList()
}
// --- Permissions
/* private fun checkPermissions(): Boolean {
val permission = if (Build.VERSION.SDK_INT >= 33)
android.Manifest.permission.READ_MEDIA_AUDIO
else
android.Manifest.permission.READ_EXTERNAL_STORAGE
return if (checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) {
true
} else {
requestPermissions(arrayOf(permission), 123)
false
}
}
*/
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == 123 &&
grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED
) {
recreate()
}
}
// --- START ---
private fun startSound(minutes: Int, status: TextView, volumeBar: ProgressBar) { private fun startSound(minutes: Int, status: TextView, volumeBar: ProgressBar) {
stopSound(status, volumeBar) stopSound(status, volumeBar)
streamId = soundPool.play(soundId, 1f, 1f, 1, -1, 1f)
if (audioFiles.isEmpty()) return
val file = audioFiles[selectedFileIndex]
if (!file.exists()) return
player = ExoPlayer.Builder(this).build().apply {
val mediaItem = MediaItem.fromUri(file.toURI().toString())
setMediaItem(mediaItem)
repeatMode = Player.REPEAT_MODE_ONE // 🔥 GAPLESS LOOP
prepare()
play()
}
isPlaying = true isPlaying = true
startTime = System.currentTimeMillis() startTime = System.currentTimeMillis()
if (minutes > 0) { if (minutes > 0) {
totalMillis = minutes * 60_000L totalMillis = minutes * 60_000L
updateCountdown(minutes * 60, status) updateCountdown(minutes * 60, status)
// Countdown jede Sekunde
countdownRunnable = object : Runnable { countdownRunnable = object : Runnable {
override fun run() { override fun run() {
val elapsedSec = ((System.currentTimeMillis() - startTime) / 1000).toInt() val elapsedSec = ((System.currentTimeMillis() - startTime) / 1000).toInt()
val remainingSec = max(minutes * 60 - elapsedSec, 0) val remainingSec = max(minutes * 60 - elapsedSec, 0)
updateCountdown(remainingSec, status) updateCountdown(remainingSec, status)
if (remainingSec > 0) { if (remainingSec > 0) {
handler.postDelayed(this, 1000L) handler.postDelayed(this, 1000L)
} else { } else {
@@ -86,16 +199,18 @@ class MainActivity : Activity() {
} }
handler.postDelayed(countdownRunnable!!, 1000L) handler.postDelayed(countdownRunnable!!, 1000L)
// Fade-Out (optional)
if (fadeEnabled) { if (fadeEnabled) {
fadeRunnable = object : Runnable { fadeRunnable = object : Runnable {
override fun run() { override fun run() {
if (!isPlaying) return if (!isPlaying) return
val elapsed = System.currentTimeMillis() - startTime val elapsed = System.currentTimeMillis() - startTime
val progress = (elapsed.toFloat() / totalMillis).coerceIn(0f, 1f) val progress = (elapsed.toFloat() / totalMillis).coerceIn(0f, 1f)
val volume = (1 - progress).pow(2) // exponentielles Fade val volume = (1 - progress).pow(2)
soundPool.setVolume(streamId, volume, volume)
player?.volume = volume
volumeBar.progress = (volume * 100).toInt() volumeBar.progress = (volume * 100).toInt()
if (progress < 1f) { if (progress < 1f) {
handler.postDelayed(this, 200L) handler.postDelayed(this, 200L)
} }
@@ -105,32 +220,38 @@ class MainActivity : Activity() {
} else { } else {
volumeBar.progress = 100 volumeBar.progress = 100
} }
} else { } else {
// unendlich updateUI(status, volumeBar, "Stopp: ∞", Color.RED, 1f)
updateUI(status, volumeBar, "Läuft: ∞", Color.GREEN, 1f)
volumeBar.progress = 100 volumeBar.progress = 100
} }
} }
// --- Countdown ---
private fun updateCountdown(remainingSec: Int, status: TextView) { private fun updateCountdown(remainingSec: Int, status: TextView) {
val minutes = remainingSec / 60 val minutes = remainingSec / 60
val seconds = remainingSec % 60 val seconds = remainingSec % 60
status.text = String.format("Läuft: %02d:%02d", minutes, seconds) status.text = String.format("Stopp: %02d:%02d", minutes, seconds)
status.setBackgroundColor(Color.GREEN) status.setBackgroundColor(Color.RED)
status.setTextColor(Color.BLACK) status.setTextColor(Color.BLACK)
} }
// --- STOP ---
private fun stopSound(status: TextView, volumeBar: ProgressBar) { private fun stopSound(status: TextView, volumeBar: ProgressBar) {
if (isPlaying) { if (isPlaying) {
soundPool.stop(streamId) player?.release()
player = null
isPlaying = false isPlaying = false
} }
handler.removeCallbacks(countdownRunnable ?: Runnable {}) handler.removeCallbacks(countdownRunnable ?: Runnable {})
handler.removeCallbacks(fadeRunnable ?: Runnable {}) handler.removeCallbacks(fadeRunnable ?: Runnable {})
updateUI(status, volumeBar, "Gestoppt", Color.RED, 0f)
updateUI(status, volumeBar, "Start", Color.GREEN, 0f)
volumeBar.progress = 0 volumeBar.progress = 0
} }
// --- UI ---
private fun updateUI(view: TextView, bar: ProgressBar, text: String, color: Int, volume: Float) { private fun updateUI(view: TextView, bar: ProgressBar, text: String, color: Int, volume: Float) {
view.text = text view.text = text
view.setBackgroundColor(color) view.setBackgroundColor(color)
@@ -140,6 +261,6 @@ class MainActivity : Activity() {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
soundPool.release() player?.release()
} }
} }
+12 -2
View File
@@ -19,9 +19,18 @@
<Space <Space
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="50dp" /> android:layout_height="50dp" />
<Spinner
android:id="@+id/mp3Spinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<Space
android:layout_width="match_parent"
android:layout_height="20dp" />
<TextView <TextView
android:id="@+id/statusText" android:id="@+id/statusText"
android:text="Bereit" android:text="Start"
android:textSize="26sp" android:textSize="26sp"
android:gravity="center" android:gravity="center"
android:textColor="#000000" android:textColor="#000000"
@@ -33,6 +42,7 @@
<EditText <EditText
android:id="@+id/timerInput" android:id="@+id/timerInput"
android:hint="Sleep Timer (Minuten)" android:hint="Sleep Timer (Minuten)"
android:text="20"
android:inputType="number" android:inputType="number"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -40,7 +50,7 @@
<TextView <TextView
android:id="@+id/fadeToggle" android:id="@+id/fadeToggle"
android:text="Fade-Out: OFF" android:text="Fade-Out: ON"
android:gravity="center" android:gravity="center"
android:padding="12dp" android:padding="12dp"
android:layout_marginTop="20dp" android:layout_marginTop="20dp"
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.