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+ }
0 commit comments