Skip to content

Commit

Permalink
Merge pull request #24 from nai-kon/Ver3.5.0
Browse files Browse the repository at this point in the history
Ver3.5.0
  • Loading branch information
nai-kon authored Sep 13, 2024
2 parents e2c66d3 + 160b09d commit 2b1bc71
Show file tree
Hide file tree
Showing 35 changed files with 1,420 additions and 572 deletions.
606 changes: 566 additions & 40 deletions 3rd-party-license.txt

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# PlaySK Piano Roll Reader Ver3.4
# PlaySK Piano Roll Reader Ver3.5

Optically reading a piano roll image, emulates expression and output midi signal in real-time.

Expand All @@ -7,7 +7,6 @@ Optically reading a piano roll image, emulates expression and output midi signal
The "virtual tracker bar" optically picks up roll holes then emulates note, pedal and expression.
Currently, 9 virtual tracker bars are available.
- Standard 88-note
- Themodist
- Ampico B
- Duo-Art
- Welte-Mignon Licensee
Expand All @@ -16,6 +15,8 @@ Currently, 9 virtual tracker bars are available.
- Philipps Duca (no expression. experimental)
- Recordo version A / B
- Artecho
- Themodist
- `Themodist e-Valve` supports e-valve midi note output. (18 for sustain, 19 for bass snakebite, 109 for treble snakebite)

In the future, Ampico A will be supported.

Expand Down
2 changes: 1 addition & 1 deletion build_mac.spec
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,5 @@ app = BUNDLE(
name='PlaySK Piano Roll Reader.app',
icon='src/playsk_config/PlaySK_icon.ico',
bundle_identifier=None,
version='3.4.0'
version='3.5.0'
)
803 changes: 432 additions & 371 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "PlaySK-Piano-Roll-Reader"
version = "3.4.0"
version = "3.5.0"
description = "Optically reading a piano roll image, emulates expression and output midi signal in real-time."
authors = ["nai-kon <fxtch686@yahoo.co.jp>"]
readme = "README.md"
Expand Down
2 changes: 1 addition & 1 deletion src/build_mac.spec
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,5 @@ app = BUNDLE(
name='PlaySK Piano Roll Reader.app',
icon='src/config/PlaySK_icon.ico',
bundle_identifier="com.KatzSasaki.PlaySK",
version='3.4.0'
version='3.5.0'
)
32 changes: 13 additions & 19 deletions src/cis_decoder/cis_decoder.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ cdef enum CurColor:
@cython.wraparound(False)
@cython.cdivision(True)
def _get_decode_params(cnp.ndarray[cnp.uint16_t, ndim=1] data,
int vert_px, int hol_px, int overlap_twin, int lpt,
bint is_bicolor, bint is_twin_array, bint is_clocked):
int vert_px, int hol_px, int lpt, bint is_bicolor,
bint is_twin_array, bint is_clocked, int twin_array_overlap):

cdef:
int width = hol_px
Expand All @@ -39,7 +39,7 @@ def _get_decode_params(cnp.ndarray[cnp.uint16_t, ndim=1] data,

# width
if is_twin_array:
width = hol_px * 2 - overlap_twin
width = hol_px * 2 - twin_array_overlap

# height
if is_clocked:
Expand Down Expand Up @@ -86,38 +86,36 @@ def _get_decode_params(cnp.ndarray[cnp.uint16_t, ndim=1] data,
@cython.boundscheck(False)
@cython.wraparound(False)
def _decode_cis(cnp.ndarray[cnp.uint16_t, ndim=1] data,
cnp.ndarray[cnp.uint8_t, ndim=3] out_img,
int vert_px, int hol_px, int twin_overlap, int twin_vsep, int end_padding_y,
bint is_bicolor, bint is_twin_array, bint is_clocked, list reclock_map):
cnp.ndarray[cnp.uint8_t, ndim=2] out_img,
int vert_px, int hol_px, bint is_bicolor, bint is_twin_array, bint is_clocked,
int twin_array_overlap, int twin_array_vsep, int end_padding_y, list reclock_map):

# CIS file format
# http://semitone440.co.uk/rolls/utils/cisheader/cis-format.htm#scantype

cdef:
cnp.uint8_t bg_color = 255
cnp.uint8_t black_color = 0
cnp.uint8_t lyrics_color = 0
int cur_idx = 0
int last_pos = 0
CurColor cur_pix = ROLL
int change_len
int i
int cur_line
int cur_line_twin
int twin_offset_x = hol_px - twin_overlap
int twin_offset_x = hol_px - int(twin_array_overlap / 2)
int sx
int ex

# decode lines
for cur_line in range(vert_px + end_padding_y- 1, end_padding_y, -1):
for cur_line in range(vert_px + end_padding_y - 1, end_padding_y, -1):
last_pos = 0
cur_pix = ROLL
while last_pos != hol_px:
change_len = data[cur_idx]
if cur_pix == BG:
for i in range(last_pos, last_pos + change_len):
out_img[cur_line, i, 0] = bg_color
out_img[cur_line, i, 1] = bg_color
out_img[cur_line, i, 2] = bg_color
out_img[cur_line, i] = bg_color
cur_pix = ROLL
elif cur_pix == ROLL:
cur_pix = BG
Expand All @@ -129,16 +127,14 @@ def _decode_cis(cnp.ndarray[cnp.uint16_t, ndim=1] data,
if is_twin_array:
last_pos = 0
cur_pix = ROLL
cur_line_twin = cur_line + twin_vsep
cur_line_twin = cur_line + twin_array_vsep
while last_pos != hol_px:
change_len = data[cur_idx]
if cur_pix == BG:
sx = 2 * twin_offset_x - min(last_pos + change_len, twin_offset_x)
ex = 2 * twin_offset_x - min(last_pos, twin_offset_x)
for i in range(sx, ex):
out_img[cur_line_twin, i, 0] = bg_color
out_img[cur_line_twin, i, 1] = bg_color
out_img[cur_line_twin, i, 2] = bg_color
out_img[cur_line_twin, i] = bg_color
cur_pix = ROLL
elif cur_pix == ROLL:
cur_pix = BG
Expand All @@ -154,9 +150,7 @@ def _decode_cis(cnp.ndarray[cnp.uint16_t, ndim=1] data,
change_len = data[cur_idx]
if cur_pix == MARK:
for i in range(last_pos, last_pos + change_len):
out_img[cur_line, i, 0] = black_color
out_img[cur_line, i, 1] = black_color
out_img[cur_line, i, 2] = black_color
out_img[cur_line, i] = lyrics_color
cur_pix = BG
elif cur_pix == BG:
cur_pix = MARK
Expand Down
107 changes: 58 additions & 49 deletions src/cis_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@ class ScannerType(Enum):

class CisImage:
"""
Decode .CIS file to numpy array. Support bi-color, twin-array, encoder scanner, stepper scanner.
Decode .CIS file to grayscale image of numpy array. Support bi-color, twin-array, encoder scanner, stepper scanner.
The encoder scanner file will re-clocked to stepper scanner. Finally the vertical lpi will resize to same size of horizontal dpi.
# CIS file format
# http://semitone440.co.uk/rolls/utils/cisheader/cis-format.htm#scantype
"""
def __init__(self) -> None:
self.desc = ""
Expand All @@ -39,19 +42,16 @@ def __init__(self) -> None:
self.is_twin_array = False
self.is_bicolor = False
self.encoder_division = 1
# self.mirror = False # not used now
# self.reverse = False
# self.clock_doubler = False
self.vert_sep_twin = 0
self.overlap_twin = 0
self.twin_array_vert_sep = 0
self.twin_array_overlap = 0
self.hol_dpi = 0
self.hol_px = 0
self.tempo = 0
self.vert_res = 0 # lpi in stepper scanner. tpi in encoder wheel
self.vert_res = 0 # lpi in stepper scanner. tpi in encoder wheel scanner
self.vert_px = 0
self.raw_img = None
self.decode_img = None
self.lpt = 0 # lines per tick. only for re-clock
self.decoded_img = None
self.lpt = 0 # lines per tick. only for re-clocking

def load(self, path: str) -> bool:
try:
Expand All @@ -63,7 +63,15 @@ def load(self, path: str) -> bool:

return False

def _get_decode_params_py(self, data: np.ndarray) -> tuple[int, int, list[int, int]]:
def convert_bw(self):
"""
Some cis files are scanned with a black roll background, so convert it to white
"""

if self.decoded_img is not None:
self.decoded_img[self.decoded_img == 0] = 255

def _get_decode_params_py(self) -> tuple[int, int, list[int, int]]:
"""
Experimentally decode file and get output image size and re-clock position map.
Python version.
Expand All @@ -73,7 +81,7 @@ def _get_decode_params_py(self, data: np.ndarray) -> tuple[int, int, list[int, i
# width
width = self.hol_px
if self.is_twin_array:
width = self.hol_px * 2 - self.overlap_twin
width = self.hol_px * 2 - self.twin_array_overlap

# height
height = self.vert_px
Expand All @@ -88,11 +96,11 @@ def _get_decode_params_py(self, data: np.ndarray) -> tuple[int, int, list[int, i
for _ in range(chs):
last_pos = 0
while last_pos != self.hol_px:
change_len = data[cur_idx]
change_len = self.raw_img[cur_idx]
cur_idx += 1
last_pos += change_len

encoder_val = data[cur_idx]
encoder_val = self.raw_img[cur_idx]
# clock = bool(encoder_val & 32)
state = bool(encoder_val & 128)
if (pre_state != state or cur_line == 0) and buf_lines:
Expand Down Expand Up @@ -122,43 +130,43 @@ def _get_decode_params_py(self, data: np.ndarray) -> tuple[int, int, list[int, i
return width, height, reclock_map

# slow python version. only used for debugging
def _decode_cis_py(self, data, out_img, vert_px, hol_px, twin_overlap, twin_vsep, end_padding_y, bicolor, twin, reclock_map) -> None:
def _decode_cis_py(self, output_img, end_padding_y, twin_array_vert_sep, reclock_map) -> None:
class CurColor(IntEnum):
BG = auto()
ROLL = auto()
MARK = auto()

# decode CIS
bg_color = (255, 255, 255)
black_color = (0, 0, 0)
bg_color = 255
lyrics_color = 0
cur_idx = 0
twin_offset_x = hol_px - twin_overlap
for cur_line in range(vert_px + end_padding_y - 1, end_padding_y, -1):
twin_offset_x = self.hol_px - self.twin_array_overlap // 2
for cur_line in range(self.vert_px + end_padding_y - 1, end_padding_y, -1):
# decode holes
last_pos = 0
cur_pix = CurColor.ROLL
while last_pos != hol_px:
change_len = data[cur_idx]
while last_pos != self.hol_px:
change_len = self.raw_img[cur_idx]
if cur_pix == CurColor.BG:
out_img[cur_line, last_pos:last_pos + change_len] = bg_color
output_img[cur_line, last_pos:last_pos + change_len] = bg_color
cur_pix = CurColor.ROLL
elif cur_pix == CurColor.ROLL:
cur_pix = CurColor.BG

cur_idx += 1
last_pos += change_len

# decode twin-array right
if twin:
# decode twin-array image
if self.is_twin_array:
last_pos = 0
cur_pix = CurColor.ROLL
cur_line_twin = cur_line + twin_vsep
while last_pos != hol_px:
change_len = data[cur_idx]
cur_line_twin = cur_line + twin_array_vert_sep
while last_pos != self.hol_px:
change_len = self.raw_img[cur_idx]
if cur_pix == CurColor.BG:
sx = 2 * twin_offset_x - min(last_pos + change_len, twin_offset_x)
ex = 2 * twin_offset_x - min(last_pos, twin_offset_x)
out_img[cur_line_twin, sx:ex] = bg_color
output_img[cur_line_twin, sx:ex] = bg_color
cur_pix = CurColor.ROLL
elif cur_pix == CurColor.ROLL:
cur_pix = CurColor.BG
Expand All @@ -167,27 +175,26 @@ class CurColor(IntEnum):
last_pos += change_len

# decode lyrics
if bicolor:
if self.is_bicolor:
last_pos = 0
cur_pix = CurColor.MARK
while last_pos != hol_px:
change_len = data[cur_idx]
while last_pos != self.hol_px:
change_len = self.raw_img[cur_idx]
if cur_pix == CurColor.MARK:
out_img[cur_line, last_pos:last_pos + change_len] = black_color
output_img[cur_line, last_pos:last_pos + change_len] = lyrics_color
cur_pix = CurColor.BG
elif cur_pix == CurColor.BG:
cur_pix = CurColor.MARK

cur_idx += 1
last_pos += change_len

# encoder_val = data[cur_idx] # not used
cur_idx += 1

if self.is_clocked:
# reposition lines
for src, dest in reclock_map:
out_img[dest + end_padding_y] = out_img[src + end_padding_y]
output_img[dest + end_padding_y] = output_img[src + end_padding_y]

def _load_file(self, path: str) -> None:
# CIS file format
Expand All @@ -212,18 +219,18 @@ def _load_file(self, path: str) -> None:
if self.scanner_type == ScannerType.UNKNOWN:
self.hol_dpi = 200 # most of unknown type scanner is 200DPI
else:
self.is_clocked = self.scanner_type in [ScannerType.WHEELENCODER, ScannerType.SHAFTENCODER]
self.is_clocked = self.scanner_type in (ScannerType.WHEELENCODER, ScannerType.SHAFTENCODER)
self.is_twin_array = bool(status_flags[0] & 32)
self.is_bicolor = bool(status_flags[0] & 64)
self.encoder_division = 2 ** int(status_flags[1] & 15)
# self.clock_doubler = bool(status_flags[0] & 16) # not used now
# self.mirror = bool(status_flags[1] & 16)
# self.reverse = bool(status_flags[1] & 32)
self.vert_sep_twin = int.from_bytes(data[36:38], byteorder="little")
self.twin_array_vert_sep = int.from_bytes(data[36:38], byteorder="little")
self.hol_dpi = int.from_bytes(data[38:40], byteorder="little")

self.hol_px = int.from_bytes(data[40:42], byteorder="little")
self.overlap_twin = int.from_bytes(data[42:44], byteorder="little")
self.twin_array_overlap = int.from_bytes(data[42:44], byteorder="little")
self.tempo = int.from_bytes(data[44:46], byteorder="little")
self.vert_res = int.from_bytes(data[46:48], byteorder="little")
self.vert_px = int.from_bytes(data[48:52], byteorder="little")
Expand All @@ -236,32 +243,34 @@ def _load_file(self, path: str) -> None:
self.vert_res = round(self.lpt * division)

def _decode(self, use_cython=True) -> None:
# get output image params
out_w, out_h, reclock_map = _get_decode_params(self.raw_img, self.vert_px, self.hol_px, self.overlap_twin, self.lpt, self.is_bicolor, self.is_twin_array, self.is_clocked)
# out_w, out_h, reclock_map = self.get_decode_params_py(self.raw_img)
# get actual output image size and reclock info
if use_cython:
out_w, out_h, reclock_map = _get_decode_params(self.raw_img, self.vert_px, self.hol_px, self.lpt, self.is_bicolor, self.is_twin_array, self.is_clocked, self.twin_array_overlap)
else:
out_w, out_h, reclock_map = self._get_decode_params_py()

twin_overlap = self.overlap_twin // 2
twin_vsep = math.ceil(self.vert_sep_twin * self.vert_res / 1000)
twin_array_vert_sep = math.ceil(self.twin_array_vert_sep * self.vert_res / 1000)

# reserve decoded image with padding on start/end
start_padding_y = out_w // 2
end_padding_y = out_w // 2
self.decode_img = np.full((out_h + start_padding_y + end_padding_y, out_w, 3), 120, np.uint8)
self.decode_img[out_h + end_padding_y:] = 255
self.decoded_img = np.full((out_h + start_padding_y + end_padding_y, out_w), 120, np.uint8)
self.decoded_img[out_h + end_padding_y:] = 255

# decode
if use_cython:
_decode_cis(self.raw_img, self.decode_img, self.vert_px, self.hol_px, twin_overlap, twin_vsep, end_padding_y, self.is_bicolor, self.is_twin_array, self.is_clocked, reclock_map)
_decode_cis(self.raw_img, self.decoded_img, self.vert_px, self.hol_px, self.is_bicolor, self.is_twin_array, self.is_clocked,
self.twin_array_overlap, twin_array_vert_sep, end_padding_y, reclock_map)
else:
self._decode_cis_py(self.raw_img, self.decode_img, self.vert_px, self.hol_px, twin_overlap, twin_vsep, end_padding_y, self.is_bicolor, self.is_twin_array, reclock_map)
self._decode_cis_py(self.decoded_img, end_padding_y, twin_array_vert_sep, reclock_map)

if len(self.decode_img) == 0:
if len(self.decoded_img) == 0:
raise BufferError

# Resize horizontal and vertical to the same dpi
self.hol_px = self.decode_img.shape[1]
self.hol_px = self.decoded_img.shape[1]
if self.vert_res != self.hol_dpi:
self.decode_img = cv2.resize(self.decode_img, dsize=None, fx=1, fy=self.hol_dpi / self.vert_res)
self.decoded_img = cv2.resize(self.decoded_img, dsize=None, fx=1, fy=self.hol_dpi / self.vert_res, interpolation=cv2.INTER_LINEAR_EXACT)


if __name__ == "__main__":
Expand All @@ -270,4 +279,4 @@ def _decode(self, use_cython=True) -> None:
obj = CisImage()
if obj.load("../sample_Scans/Ampico B 68991 Papillons.CIS"):
print(time.time() - s)
cv2.imwrite("decoded_cis.png", obj.decode_img)
cv2.imwrite("decoded_cis.png", obj.decoded_img)
Loading

0 comments on commit 2b1bc71

Please sign in to comment.