Skip to content

Commit d3be0ce

Browse files
AudioPlayer: upscale if low frequency
1 parent 1984857 commit d3be0ce

File tree

1 file changed

+70
-22
lines changed
  • internal_filesystem/apps/com.micropythonos.musicplayer/assets

1 file changed

+70
-22
lines changed

internal_filesystem/apps/com.micropythonos.musicplayer/assets/audio_player.py

Lines changed: 70 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,19 @@
66

77
# ----------------------------------------------------------------------
88
# AudioPlayer – robust, volume-controllable WAV player (MONO + STEREO)
9+
# Auto-up-samples any rate < 22050 Hz to 22050 Hz for MAX98357
910
# ----------------------------------------------------------------------
1011
class AudioPlayer:
11-
# class-level defaults
1212
_i2s = None
13-
_volume = 50 # 0-100
13+
_volume = 50 # 0-100
1414
_keep_running = True
1515

16+
# ------------------------------------------------------------------
17+
# WAV header parser
18+
# ------------------------------------------------------------------
1619
@staticmethod
1720
def find_data_chunk(f):
18-
"""Skip chunks until 'data' is found → (data_start, data_size, sample_rate, channels)"""
21+
"""Return (data_start, data_size, sample_rate, channels)"""
1922
f.seek(0)
2023
if f.read(4) != b'RIFF':
2124
raise ValueError("Not a RIFF file")
@@ -68,6 +71,34 @@ def stop_playing(cls):
6871
print("stop_playing()")
6972
cls._keep_running = False
7073

74+
# ------------------------------------------------------------------
75+
# Helper: up-sample a raw PCM buffer (zero-order-hold)
76+
# ------------------------------------------------------------------
77+
@staticmethod
78+
def _upsample_buffer(raw: bytearray, factor: int) -> bytearray:
79+
"""
80+
Duplicate each 16-bit sample `factor` times.
81+
Input: interleaved L,R,L,R... (or mono)
82+
Output: same layout, each sample repeated `factor` times.
83+
"""
84+
if factor == 1:
85+
return raw
86+
87+
upsampled = bytearray(len(raw) * factor)
88+
out_idx = 0
89+
# each sample = 2 bytes
90+
for i in range(0, len(raw), 2):
91+
lo = raw[i]
92+
hi = raw[i + 1]
93+
for _ in range(factor):
94+
upsampled[out_idx] = lo
95+
upsampled[out_idx + 1] = hi
96+
out_idx += 2
97+
return upsampled
98+
99+
# ------------------------------------------------------------------
100+
# Main playback routine
101+
# ------------------------------------------------------------------
71102
@classmethod
72103
def play_wav(cls, filename):
73104
cls._keep_running = True
@@ -77,15 +108,28 @@ def play_wav(cls, filename):
77108
file_size = st[6]
78109
print(f"File size: {file_size} bytes")
79110

80-
data_start, data_size, sample_rate, channels = cls.find_data_chunk(f)
81-
print(f"data chunk: {data_size} bytes @ {sample_rate} Hz, {channels}-channel")
111+
# ----- parse header ------------------------------------------------
112+
data_start, data_size, original_rate, channels = cls.find_data_chunk(f)
113+
114+
# ----- decide playback rate (force >= 22050 Hz) --------------------
115+
target_rate = 22050
116+
if original_rate >= target_rate:
117+
playback_rate = original_rate
118+
upsample_factor = 1
119+
else:
120+
# find smallest integer factor so original * factor >= target
121+
upsample_factor = (target_rate + original_rate - 1) // original_rate
122+
playback_rate = original_rate * upsample_factor
123+
124+
print(f"Original: {original_rate} Hz → Playback: {playback_rate} Hz "
125+
f"(factor {upsample_factor}), {channels}-ch")
82126

83127
if data_size > file_size - data_start:
84128
data_size = file_size - data_start
85129

86-
# ---- I2S init ------------------------------------------------
87-
i2s_format = machine.I2S.MONO if channels == 1 else machine.I2S.STEREO
130+
# ----- I2S init ----------------------------------------------------
88131
try:
132+
i2s_format = machine.I2S.MONO if channels == 1 else machine.I2S.STEREO
89133
cls._i2s = machine.I2S(
90134
0,
91135
sck=machine.Pin(2, machine.Pin.OUT),
@@ -94,18 +138,18 @@ def play_wav(cls, filename):
94138
mode=machine.I2S.TX,
95139
bits=16,
96140
format=i2s_format,
97-
rate=sample_rate,
141+
rate=playback_rate,
98142
ibuf=32000
99143
)
100144
except Exception as e:
101145
print(f"Warning: simulating playback (I2S init failed): {e}")
102146

103-
print(f"Playing {data_size} bytes (vol {cls._volume}%) …")
147+
print(f"Playing {data_size} original bytes (vol {cls._volume}%) …")
104148
f.seek(data_start)
105149

150+
# ----- Viper volume scaler (works on any buffer) -------------------
106151
@micropython.viper
107152
def scale_audio(buf: ptr8, num_bytes: int, scale_fixed: int):
108-
# Process 16-bit samples (2 bytes each)
109153
for i in range(0, num_bytes, 2):
110154
lo = int(buf[i])
111155
hi = int(buf[i+1])
@@ -121,39 +165,43 @@ def scale_audio(buf: ptr8, num_bytes: int, scale_fixed: int):
121165
buf[i+1] = (sample >> 8) & 255
122166

123167
chunk_size = 4096
124-
bytes_per_sample = 2 * channels # 2 bytes per channel
125-
total = 0
168+
bytes_per_original_sample = 2 * channels # 2 bytes per channel
169+
total_original = 0
126170

127-
while total < data_size:
171+
while total_original < data_size:
128172
if not cls._keep_running:
129173
print("Playback stopped by user.")
130174
break
131175

132-
to_read = min(chunk_size, data_size - total)
133-
# Ensure we read full samples
134-
to_read -= (to_read % bytes_per_sample)
176+
# read a chunk of *original* data
177+
to_read = min(chunk_size, data_size - total_original)
178+
to_read -= (to_read % bytes_per_original_sample)
135179
if to_read <= 0:
136180
break
137181

138182
raw = bytearray(f.read(to_read))
139183
if not raw:
140184
break
141185

142-
# Apply volume scaling (in-place, per sample)
186+
# ----- up-sample if needed ---------------------------------
187+
if upsample_factor > 1:
188+
raw = cls._upsample_buffer(raw, upsample_factor)
189+
190+
# ----- volume scaling ---------------------------------------
143191
scale = cls._volume / 100.0
144192
if scale < 1.0:
145193
scale_fixed = int(scale * 32768)
146194
scale_audio(raw, len(raw), scale_fixed)
147195

148-
# Write to I2S (stereo interleaves L,R,L,R...)
196+
# ----- output ------------------------------------------------
149197
if cls._i2s:
150198
cls._i2s.write(raw)
151199
else:
152-
# Simulate timing
153-
num_samples = len(raw) // bytes_per_sample
154-
time.sleep(num_samples / sample_rate)
200+
# simulate timing with the *playback* rate
201+
num_samples = len(raw) // (2 * channels)
202+
time.sleep(num_samples / playback_rate)
155203

156-
total += len(raw)
204+
total_original += to_read # count original bytes only
157205

158206
print("Playback finished.")
159207
except Exception as e:

0 commit comments

Comments
 (0)