Skip to content

Commit b056c77

Browse files
MusicPlayer app: Move AudioPlayer to its own class
1 parent f8ad2a0 commit b056c77

File tree

2 files changed

+139
-99
lines changed

2 files changed

+139
-99
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import os
2+
import time
3+
4+
# ----------------------------------------------------------------------
5+
# AudioPlayer – robust, volume-controllable WAV player
6+
# ----------------------------------------------------------------------
7+
class AudioPlayer:
8+
# class-level defaults (shared by every instance)
9+
_i2s = None # the I2S object (created once per playback)
10+
_volume = 100 # 0-100 (100 = full scale)
11+
12+
@staticmethod
13+
def find_data_chunk(f):
14+
"""Skip chunks until 'data' is found → (data_start, data_size, sample_rate)"""
15+
f.seek(0)
16+
if f.read(4) != b'RIFF':
17+
raise ValueError("Not a RIFF file")
18+
file_size = int.from_bytes(f.read(4), 'little') + 8
19+
if f.read(4) != b'WAVE':
20+
raise ValueError("Not a WAVE file")
21+
22+
pos = 12
23+
sample_rate = None
24+
while pos < file_size:
25+
f.seek(pos)
26+
chunk_id = f.read(4)
27+
if len(chunk_id) < 4:
28+
break
29+
chunk_size = int.from_bytes(f.read(4), 'little')
30+
if chunk_id == b'fmt ':
31+
fmt = f.read(chunk_size)
32+
if len(fmt) < 16:
33+
raise ValueError("Invalid fmt chunk")
34+
if int.from_bytes(fmt[0:2], 'little') != 1:
35+
raise ValueError("Only PCM supported")
36+
channels = int.from_bytes(fmt[2:4], 'little')
37+
if channels != 1:
38+
raise ValueError("Only mono supported")
39+
sample_rate = int.from_bytes(fmt[4:8], 'little')
40+
if int.from_bytes(fmt[14:16], 'little') != 16:
41+
raise ValueError("Only 16-bit supported")
42+
elif chunk_id == b'data':
43+
return f.tell(), chunk_size, sample_rate
44+
# next chunk (pad byte if odd length)
45+
pos += 8 + chunk_size
46+
if chunk_size % 2:
47+
pos += 1
48+
raise ValueError("No 'data' chunk found")
49+
50+
# ------------------------------------------------------------------
51+
# Volume control
52+
# ------------------------------------------------------------------
53+
@classmethod
54+
def set_volume(cls, volume: int):
55+
"""Set playback volume 0-100 (100 = full scale)."""
56+
volume = max(0, min(100, volume)) # clamp
57+
cls._volume = volume
58+
# If playback is already running we could instantly re-scale the
59+
# current buffer, but the simple way (scale on each write) is
60+
# enough and works even if playback starts later.
61+
62+
@classmethod
63+
def get_volume(cls) -> int:
64+
"""Return current volume 0-100."""
65+
return cls._volume
66+
67+
# ------------------------------------------------------------------
68+
# Playback entry point (called from a thread)
69+
# ------------------------------------------------------------------
70+
@classmethod
71+
def play_wav(cls, filename):
72+
"""Play a large mono 16-bit PCM WAV file with on-the-fly volume."""
73+
try:
74+
with open(filename, 'rb') as f:
75+
st = os.stat(filename)
76+
file_size = st[6]
77+
print(f"File size: {file_size} bytes")
78+
79+
data_start, data_size, sample_rate = cls.find_data_chunk(f)
80+
print(f"data chunk: {data_size} bytes @ {sample_rate} Hz")
81+
82+
if data_size > file_size - data_start:
83+
data_size = file_size - data_start
84+
85+
# ---- I2S init ------------------------------------------------
86+
try:
87+
cls._i2s = machine.I2S(
88+
0,
89+
sck=machine.Pin(2, machine.Pin.OUT),
90+
ws =machine.Pin(47, machine.Pin.OUT),
91+
sd =machine.Pin(16, machine.Pin.OUT),
92+
mode=machine.I2S.TX,
93+
bits=16,
94+
format=machine.I2S.MONO,
95+
rate=sample_rate,
96+
ibuf=32000
97+
)
98+
except Exception as e:
99+
print("Warning: error initializing I2S audio device, simulating playback...")
100+
101+
print(f"Playing {data_size} bytes (vol {cls._volume}%) …")
102+
f.seek(data_start)
103+
104+
chunk_size = 4096 # 4 KB → safe on ESP32
105+
scale = cls._volume / 100.0 # float 0.0-1.0
106+
107+
total = 0
108+
while total < data_size:
109+
to_read = min(chunk_size, data_size - total)
110+
raw = f.read(to_read)
111+
if not raw:
112+
break
113+
114+
# ---- on-the-fly volume scaling (16-bit little-endian) ----
115+
if scale < 1.0:
116+
# convert bytes → array of signed ints → scale → back to bytes
117+
import array
118+
samples = array.array('h', raw) # 'h' = signed short
119+
for i in range(len(samples)):
120+
samples[i] = int(samples[i] * scale)
121+
raw = samples.tobytes()
122+
# ---------------------------------------------------------
123+
124+
if cls._i2s:
125+
cls._i2s.write(raw)
126+
else:
127+
time.sleep((to_read/2)/44100) # 16 bits (2 bytes) per sample at 44100 samples/s
128+
total += len(raw)
129+
130+
print("Playback finished.")
131+
except Exception as e:
132+
print(f"AudioPlayer error: {e}")
133+
finally:
134+
if cls._i2s:
135+
cls._i2s.deinit()
136+
cls._i2s = None

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

Lines changed: 3 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import machine
2-
import uos
2+
import os
33
import _thread
44

55
from mpos.apps import Activity, Intent
66
import mpos.sdcard
77
import mpos.ui
88

9+
from audio_player import AudioPlayer
10+
911
class MusicPlayer(Activity):
1012

1113
# Widgets:
@@ -42,104 +44,6 @@ def file_explorer_event_cb(self, event):
4244
else:
4345
print("INFO: ignoring unsupported file format")
4446

45-
class AudioPlayer:
46-
47-
def find_data_chunk(f):
48-
"""Skip chunks until 'data' is found. Returns (data_start_pos, data_size)."""
49-
# Go back to start
50-
f.seek(0)
51-
riff = f.read(4)
52-
if riff != b'RIFF':
53-
raise ValueError("Not a RIFF file")
54-
file_size = int.from_bytes(f.read(4), 'little') + 8 # Total file size
55-
wave = f.read(4)
56-
if wave != b'WAVE':
57-
raise ValueError("Not a WAVE file")
58-
59-
pos = 12 # Start after RIFF header
60-
while pos < file_size:
61-
f.seek(pos)
62-
chunk_id = f.read(4)
63-
if len(chunk_id) < 4:
64-
break
65-
chunk_size = int.from_bytes(f.read(4), 'little')
66-
if chunk_id == b'fmt ':
67-
fmt_data = f.read(chunk_size)
68-
if len(fmt_data) < 16:
69-
raise ValueError("Invalid fmt chunk")
70-
audio_format = int.from_bytes(fmt_data[0:2], 'little')
71-
channels = int.from_bytes(fmt_data[2:4], 'little')
72-
sample_rate = int.from_bytes(fmt_data[4:8], 'little')
73-
bits_per_sample = int.from_bytes(fmt_data[14:16], 'little')
74-
if audio_format != 1:
75-
raise ValueError("Only PCM supported")
76-
if bits_per_sample != 16:
77-
raise ValueError("Only 16-bit supported")
78-
if channels != 1:
79-
raise ValueError("Only mono supported")
80-
elif chunk_id == b'data':
81-
data_start = f.tell()
82-
data_size = chunk_size
83-
return data_start, data_size, sample_rate
84-
# Skip chunk (pad byte if odd size)
85-
pos += 8 + chunk_size
86-
if chunk_size % 2 == 1:
87-
pos += 1
88-
raise ValueError("No 'data' chunk found")
89-
90-
def play_wav(filename):
91-
"""Play large WAV files robustly with chunk skipping and streaming."""
92-
try:
93-
with open(filename, 'rb') as f:
94-
stat = uos.stat(filename)
95-
file_size = stat[6]
96-
print(f"File size: {file_size} bytes")
97-
98-
data_start, data_size, sample_rate = AudioPlayer.find_data_chunk(f)
99-
print(f"Found 'data' chunk: {data_size} bytes at {sample_rate} Hz")
100-
101-
if data_size > file_size - data_start:
102-
print("Warning: data_size exceeds file bounds. Truncating.")
103-
data_size = file_size - data_start
104-
105-
# Configure I2S
106-
i2s = machine.I2S(
107-
0,
108-
sck=machine.Pin(2, machine.Pin.OUT),
109-
ws=machine.Pin(47, machine.Pin.OUT),
110-
sd=machine.Pin(16, machine.Pin.OUT),
111-
mode=machine.I2S.TX,
112-
bits=16,
113-
format=machine.I2S.MONO,
114-
rate=sample_rate,
115-
ibuf=32000 # Larger buffer for stability
116-
)
117-
118-
print(f"Playing {data_size} bytes at {sample_rate} Hz...")
119-
f.seek(data_start)
120-
121-
chunk_size = 4096 # 4KB chunks = safe for ESP32
122-
total_read = 0
123-
while total_read < data_size:
124-
remaining = data_size - total_read
125-
read_size = min(chunk_size, remaining)
126-
chunk = f.read(read_size)
127-
if not chunk:
128-
break
129-
i2s.write(chunk)
130-
total_read += len(chunk)
131-
132-
print("Playback finished.")
133-
except Exception as e:
134-
print(f"Error: {e}")
135-
finally:
136-
try:
137-
i2s.deinit()
138-
except:
139-
pass
140-
141-
142-
14347
class FullscreenPlayer(Activity):
14448
# No __init__() so super.__init__() will be called automatically
14549

0 commit comments

Comments
 (0)