Skip to content

Commit b5c33a4

Browse files
committed
Add a manual test of synthio
this allows to test how the midi synthesizer is working, without access to hardware. Run `micropython-coverage midi2wav.py` and it will create `tune.wav` as an output.
1 parent 12c1a72 commit b5c33a4

File tree

6 files changed

+787
-0
lines changed

6 files changed

+787
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
tune.wav
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Test synthio without hardware
2+
3+
Build the uninx port then run `....../ports/unix/micropython-coverage midi2wav.py`.
4+
5+
This will create `tune.wav` as output, which you can listen to using any old audio player.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import struct
2+
3+
4+
def byteswap(data, sampwidth):
5+
print(data)
6+
raise
7+
ch = "I" if sampwidth == 16 else "H"
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
"""Simple class to read IFF chunks.
2+
3+
An IFF chunk (used in formats such as AIFF, TIFF, RMFF (RealMedia File
4+
Format)) has the following structure:
5+
6+
+----------------+
7+
| ID (4 bytes) |
8+
+----------------+
9+
| size (4 bytes) |
10+
+----------------+
11+
| data |
12+
| ... |
13+
+----------------+
14+
15+
The ID is a 4-byte string which identifies the type of chunk.
16+
17+
The size field (a 32-bit value, encoded using big-endian byte order)
18+
gives the size of the whole chunk, including the 8-byte header.
19+
20+
Usually an IFF-type file consists of one or more chunks. The proposed
21+
usage of the Chunk class defined here is to instantiate an instance at
22+
the start of each chunk and read from the instance until it reaches
23+
the end, after which a new instance can be instantiated. At the end
24+
of the file, creating a new instance will fail with an EOFError
25+
exception.
26+
27+
Usage:
28+
while True:
29+
try:
30+
chunk = Chunk(file)
31+
except EOFError:
32+
break
33+
chunktype = chunk.getname()
34+
while True:
35+
data = chunk.read(nbytes)
36+
if not data:
37+
pass
38+
# do something with data
39+
40+
The interface is file-like. The implemented methods are:
41+
read, close, seek, tell, isatty.
42+
Extra methods are: skip() (called by close, skips to the end of the chunk),
43+
getname() (returns the name (ID) of the chunk)
44+
45+
The __init__ method has one required argument, a file-like object
46+
(including a chunk instance), and one optional argument, a flag which
47+
specifies whether or not chunks are aligned on 2-byte boundaries. The
48+
default is 1, i.e. aligned.
49+
"""
50+
51+
52+
class Chunk:
53+
def __init__(self, file, align=True, bigendian=True, inclheader=False):
54+
import struct
55+
56+
self.closed = False
57+
self.align = align # whether to align to word (2-byte) boundaries
58+
if bigendian:
59+
strflag = ">"
60+
else:
61+
strflag = "<"
62+
self.file = file
63+
self.chunkname = file.read(4)
64+
if len(self.chunkname) < 4:
65+
raise EOFError
66+
try:
67+
self.chunksize = struct.unpack_from(strflag + "L", file.read(4))[0]
68+
except struct.error:
69+
raise EOFError from None
70+
if inclheader:
71+
self.chunksize = self.chunksize - 8 # subtract header
72+
self.size_read = 0
73+
try:
74+
self.offset = self.file.tell()
75+
except (AttributeError, OSError):
76+
self.seekable = False
77+
else:
78+
self.seekable = True
79+
80+
def getname(self):
81+
"""Return the name (ID) of the current chunk."""
82+
return self.chunkname
83+
84+
def getsize(self):
85+
"""Return the size of the current chunk."""
86+
return self.chunksize
87+
88+
def close(self):
89+
if not self.closed:
90+
try:
91+
self.skip()
92+
finally:
93+
self.closed = True
94+
95+
def isatty(self):
96+
if self.closed:
97+
raise ValueError("I/O operation on closed file")
98+
return False
99+
100+
def seek(self, pos, whence=0):
101+
"""Seek to specified position into the chunk.
102+
Default position is 0 (start of chunk).
103+
If the file is not seekable, this will result in an error.
104+
"""
105+
106+
if self.closed:
107+
raise ValueError("I/O operation on closed file")
108+
if not self.seekable:
109+
raise OSError("cannot seek")
110+
if whence == 1:
111+
pos = pos + self.size_read
112+
elif whence == 2:
113+
pos = pos + self.chunksize
114+
if pos < 0 or pos > self.chunksize:
115+
raise RuntimeError
116+
self.file.seek(self.offset + pos, 0)
117+
self.size_read = pos
118+
119+
def tell(self):
120+
if self.closed:
121+
raise ValueError("I/O operation on closed file")
122+
return self.size_read
123+
124+
def read(self, size=-1):
125+
"""Read at most size bytes from the chunk.
126+
If size is omitted or negative, read until the end
127+
of the chunk.
128+
"""
129+
130+
if self.closed:
131+
raise ValueError("I/O operation on closed file")
132+
if self.size_read >= self.chunksize:
133+
return b""
134+
if size < 0:
135+
size = self.chunksize - self.size_read
136+
if size > self.chunksize - self.size_read:
137+
size = self.chunksize - self.size_read
138+
data = self.file.read(size)
139+
self.size_read = self.size_read + len(data)
140+
if self.size_read == self.chunksize and self.align and (self.chunksize & 1):
141+
dummy = self.file.read(1)
142+
self.size_read = self.size_read + len(dummy)
143+
return data
144+
145+
def skip(self):
146+
"""Skip the rest of the chunk.
147+
If you are not interested in the contents of the chunk,
148+
this method should be called so that the file points to
149+
the start of the next chunk.
150+
"""
151+
152+
if self.closed:
153+
raise ValueError("I/O operation on closed file")
154+
if self.seekable:
155+
try:
156+
n = self.chunksize - self.size_read
157+
# maybe fix alignment
158+
if self.align and (self.chunksize & 1):
159+
n = n + 1
160+
self.file.seek(n, 1)
161+
self.size_read = self.size_read + n
162+
return
163+
except OSError:
164+
pass
165+
while self.size_read < self.chunksize:
166+
n = min(8192, self.chunksize - self.size_read)
167+
dummy = self.read(n)
168+
if not dummy:
169+
raise EOFError
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import audiocore
2+
import synthio
3+
from ulab import numpy as np
4+
import wave
5+
6+
SAMPLE_SIZE = 1024
7+
VOLUME = 32700
8+
sine = np.array(
9+
np.sin(np.linspace(0, 2 * np.pi, SAMPLE_SIZE, endpoint=False)) * VOLUME,
10+
dtype=np.int16,
11+
)
12+
13+
envelope = synthio.Envelope(
14+
attack_time=0.1, decay_time=0.05, release_time=0.2, attack_level=1, sustain_level=0.8
15+
)
16+
melody = synthio.MidiTrack(
17+
b"\0\x90H\0*\x80H\0\6\x90J\0*\x80J\0\6\x90L\0*\x80L\0\6\x90J\0"
18+
+ b"*\x80J\0\6\x90H\0*\x80H\0\6\x90J\0*\x80J\0\6\x90L\0T\x80L\0"
19+
+ b"\x0c\x90H\0T\x80H\0\x0c\x90H\0T\x80H\0",
20+
tempo=240,
21+
sample_rate=48000,
22+
waveform=sine,
23+
envelope=envelope,
24+
)
25+
26+
# sox -r 48000 -e signed -b 16 -c 1 tune.raw tune.wav
27+
with wave.open("tune.wav", "w") as f:
28+
f.setnchannels(1)
29+
f.setsampwidth(2)
30+
f.setframerate(48000)
31+
while True:
32+
result, data = audiocore.get_buffer(melody)
33+
if data is None:
34+
break
35+
f.writeframes(data)
36+
if result != 1:
37+
break
38+
39+
melody = synthio.MidiTrack(
40+
b"\0\x90H\0*\x80H\0\6\x90J\0*\x80J\0\6\x90L\0*\x80L\0\6\x90J\0"
41+
+ b"*\x80J\0\6\x90H\0*\x80H\0\6\x90J\0*\x80J\0\6\x90L\0T\x80L\0"
42+
+ b"\x0c\x90H\0T\x80H\0\x0c\x90H\0T\x80H\0",
43+
tempo=240,
44+
sample_rate=48000,
45+
waveform=sine,
46+
)
47+
48+
# sox -r 48000 -e signed -b 16 -c 1 tune.raw tune.wav
49+
with wave.open("tune-noenv.wav", "w") as f:
50+
f.setnchannels(1)
51+
f.setsampwidth(2)
52+
f.setframerate(48000)
53+
while True:
54+
result, data = audiocore.get_buffer(melody)
55+
if data is None:
56+
break
57+
f.writeframes(data)
58+
if result != 1:
59+
break

0 commit comments

Comments
 (0)