Skip to content

Commit 95dde64

Browse files
committed
Add WsprFileManager
1 parent 7ee1a83 commit 95dde64

File tree

3 files changed

+179
-0
lines changed

3 files changed

+179
-0
lines changed

AudioCoder/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ android {
6161
}
6262

6363
dependencies {
64+
implementation(libs.timber)
6465

6566
implementation(libs.androidx.core.ktx)
6667
implementation(libs.androidx.appcompat)
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package org.operatorfoundation.audiocoder
2+
3+
import android.content.Context
4+
import android.content.Intent
5+
import android.icu.text.SimpleDateFormat
6+
import androidx.core.content.FileProvider
7+
import timber.log.Timber
8+
import java.io.File
9+
import java.io.FileOutputStream
10+
import java.nio.ByteBuffer
11+
import java.util.Date
12+
import java.util.Locale
13+
14+
class WsprFileManager(private val context: Context)
15+
{
16+
companion object
17+
{
18+
private const val SAMPLE_RATE = 12000 // WSPR uses 12kHz
19+
private const val BITS_PER_SAMPLE = 16
20+
private const val CHANNELS = 1
21+
}
22+
23+
/**
24+
* Saves WSPR Audio data as a WAV file.
25+
*
26+
* @param audioData Raw PCM audio data from WSPR encoder
27+
* @param callsign Callsign used in the WSPR message
28+
* @param gridSquare Grid square used in the WSPR message
29+
* @param power Power level used in the WSPR message
30+
* @return The saved wave file, or null if this process failed
31+
*/
32+
fun saveWsprAsWav(
33+
audioData: ByteArray,
34+
callsign: String,
35+
gridSquare: String,
36+
power: Int
37+
): File?
38+
{
39+
return try
40+
{
41+
// Create filename with timestamp and WSPR parameters
42+
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
43+
val filename = "WSPR_${callsign}_${gridSquare}_${power}dBm_$timestamp.wav"
44+
45+
// Create file in app's external files
46+
val file = File(context.getExternalFilesDir(null), filename)
47+
48+
// Write a WAV file
49+
writeWavFile(file, audioData)
50+
51+
Timber.i("WSPR WAV file saved: ${file.absolutePath}")
52+
file
53+
}
54+
catch (exception: Exception)
55+
{
56+
Timber.e(exception, "Failed to save WSPR WAV file")
57+
null
58+
}
59+
}
60+
61+
/**
62+
* Writes PCM audio data as a WAV file.
63+
*
64+
* @param file The File to save the audio data to
65+
* @audioData The audio data to save to the file
66+
*/
67+
fun writeWavFile(file: File, audioData: ByteArray)
68+
{
69+
FileOutputStream(file).use { fos ->
70+
// Calculate sizes
71+
val audioDataSize = audioData.size
72+
val fileSize = 36 + audioDataSize
73+
74+
// Create WAV header
75+
val header = ByteBuffer.allocate(44).apply {
76+
77+
// RIFF Header
78+
put("RIFF".toByteArray())
79+
putInt(fileSize)
80+
put("WAV".toByteArray())
81+
82+
// Format chunk
83+
put("fmt ".toByteArray())
84+
putInt(16) // PCM format chunk size
85+
putShort(1) // PCM format
86+
putShort(CHANNELS.toShort())
87+
putInt(SAMPLE_RATE)
88+
putInt(SAMPLE_RATE * CHANNELS * BITS_PER_SAMPLE / 8) // Byte rate
89+
putShort((CHANNELS * BITS_PER_SAMPLE / 8).toShort()) // Block align
90+
putShort(BITS_PER_SAMPLE.toShort())
91+
92+
// Data chunk
93+
put("data".toByteArray())
94+
putInt(audioDataSize)
95+
}
96+
97+
// Write header and audio data
98+
fos.write(header.array())
99+
fos.write(audioData)
100+
}
101+
}
102+
103+
104+
/**
105+
* Shares a WSPR WAV file using Android's share intent.
106+
*
107+
* @param file The file to share
108+
* @return The share intent, or null if the operation fails
109+
*/
110+
fun shareWsprFile(file: File): Intent?
111+
{
112+
return try
113+
{
114+
// Create content URI using FileProvider
115+
val uri = FileProvider.getUriForFile(
116+
context,
117+
"${context.packageName}.fileprovider",
118+
file
119+
)
120+
121+
// Create share intent
122+
val shareIntent = Intent(Intent.ACTION_SEND).apply {
123+
type = "audio/wav"
124+
putExtra(Intent.EXTRA_STREAM, uri)
125+
putExtra(Intent.EXTRA_SUBJECT, "WSPR Signal - ${file.nameWithoutExtension}")
126+
putExtra(Intent.EXTRA_TEXT, "Generated WSPR signal file")
127+
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
128+
}
129+
130+
Intent.createChooser(shareIntent, "Share WSPR Signal")
131+
}
132+
catch (exception: Exception)
133+
{
134+
Timber.e(exception, "Failed to create share intent for WSPR file")
135+
null
136+
}
137+
}
138+
139+
/**
140+
* Gets a list of saved WSPR files.
141+
*
142+
* @return A list of saved WSPR files
143+
*/
144+
fun getSavedWsprFiles(): List<File>
145+
{
146+
val directory = context.getExternalFilesDir(null) ?: return emptyList()
147+
148+
return directory.listFiles { file ->
149+
file.name.startsWith("WSPR") && file.name.endsWith(".wav")
150+
}?.toList() ?: emptyList()
151+
}
152+
153+
/**
154+
* Deletes a given WSPR file.
155+
*
156+
* @param file The file to delete
157+
*/
158+
fun deleteWsprFile(file: File): Boolean
159+
{
160+
return try
161+
{
162+
val deleted = file.delete()
163+
164+
if (deleted) {
165+
Timber.i("Deleted WSPR file: ${file.name}")
166+
}
167+
deleted
168+
}
169+
catch (exception: Exception)
170+
{
171+
Timber.e(exception, "Failed to delete WSPR file: ${file.name}")
172+
false
173+
}
174+
}
175+
}

gradle/libs.versions.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@ activityCompose = "1.8.0"
1010
composeBom = "2024.09.00"
1111
appcompat = "1.7.0"
1212
material = "1.12.0"
13+
timber = "5.0.1"
1314

1415
[libraries]
16+
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
17+
1518
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
1619
junit = { group = "junit", name = "junit", version.ref = "junit" }
1720
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }

0 commit comments

Comments
 (0)