Skip to content

Commit

Permalink
Ver(1.8): Cleanup code, //docs, reorder
Browse files Browse the repository at this point in the history
- hachitools: add Generic, rename CallFlag->SwitchCall
- support htmlColor in COLOR_BACK/COLOR_TEXT
- Add HACHIKO_DONE command hook
- BGM_VOLUME
- hachi:
 - ActionHandler interface
 - make synth global
 - extract mainloopCall
 - fix typo calmSetSFont
- hachitools:
 - rename RefUpdate.text->item
- hachi.guiReadTimeline add final noteoff & support mouse1/* as A/S
  • Loading branch information
duangsuse committed Aug 30, 2020
1 parent ed3da14 commit 679a4a6
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 58 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,5 @@ dmypy.json
*.mid
*.mp4
*.ogg
*.quicktime
*.srt~
7 changes: 4 additions & 3 deletions hachiko_bapu/cli_tools/lrc_merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
'''
This tool can convert ungrouped lrc stream to List[List[LrcNote]]
LrcNote is Tuple[int, str] (seconds, content)
...wait, it's currently Subtitle(no, start, end, content)
...wait, it's actually Subtitle(no, start=seconds, end, content)
Model: Lrc / LrcLines(Subtitle[]) / Srt
Bad model: Lrc / LrcLines / Srt
read: str -> Lrc; dump: LrcLines -> str;
into: LrcLines -> ...
str (into)<=>(from) LrcLines
'''

from datetime import timedelta
Expand Down
3 changes: 3 additions & 0 deletions hachiko_bapu/funutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
from os.path import isfile, abspath
from itertools import chain

def let(transform, value):
return transform(value) if value != None else value

def require(value, p, message):
if not p(value): raise ValueError(f"{message}: {value}")

Expand Down
80 changes: 44 additions & 36 deletions hachiko_bapu/hachi.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#!/bin/env python3
# -*- coding: utf-8 -*-

from typing import Any, Callable, Optional, TypeVar; T = TypeVar("T")
from typing import Any, Callable, Optional, TypeVar, Generic
A = TypeVar("A"); T = TypeVar("T")

from argparse import ArgumentParser, FileType
from time import time
Expand All @@ -10,31 +11,36 @@
from srt import Subtitle, compose
from json import loads, dumps, JSONDecodeError

from os import environ #v disable prompt
from os import environ, system #v disable prompt
environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide"
import pygame

from .hachitools import *
from .synthesize import NoteSynth
from pkg_resources import resource_filename
from .funutils import let

def splitAs(type, transform = int, delim = ","):
return lambda it: type(transform(s) for s in it.split(delim))

WINDOW_DIMEN = env("DIMEN", splitAs(tuple), (300,300))
NAME_STDOUT = "-"

backgroundColor = grayColor(env("GRAY_BACK", int, 0x30))
textColor = grayColor(env("GRAY_TEXT", int, 0xfa))
backgroundColor = env("COLOR_BACK", htmlColor, grayColor(0x30))
textColor = env("COLOR_TEXT", htmlColor, grayColor(0xfa))
fontName = env("FONT_NAME", str, "Arial")
fontSize = env("FONT_SIZE", int, 36)

askMethod = env("ASK_METHOD", str, "tk")
playDuration = env("PLAY_DURATION", splitAs(list, transform=float), [0.3, 0.5, 1.5])
cmdOnDone = env("HACHIKO_DONE", str, "srt2mid out")

INSTRUMENT_SF2 = env("SFONT", str, resource_filename(__name__, "instrument.sf2"))
sampleRate = env("SAMPLE_RATE", int, 44100)
sfontPreset = 0 #< used twice
synth = NoteSynth(sampleRate) #< used twice

bgmVolume = env("BGM_VOLUME", float, None)
bgmSpeed = env("BGM_SPEED", float, None) #TODO

OCTAVE_NAMES = ["C","Cs","D","Ds","E","F","Fs","G","Gs","A","As","B"]
OCTAVE_MAX_VALUE = 12
Expand Down Expand Up @@ -74,18 +80,20 @@ def blockingAskThen(onDone:Callable[[T], Any], name:str, transform:Callable[[str
app.add_argument("-play-seek", type=float, default=0.0, help="initial seek for player")
app.add_argument("-o", type=str, default="puzi.srt", help="output subtitle file path (default puzi.srt, can be - for stdout)")

class ActionHandler(Generic[A, T]):
def actions(self, ctx:A, key:T): pass

class RecordKeys(AsList):
def actions(self, ctx, k):
if k == '\x08': #<key delete
class RecordKeys(AsList, ActionHandler[RefUpdate, str]):
def actions(self, ctx, key):
if key == '\x08': #<key delete
if len(self.items) == 0: return
rm = self.items.pop()
ctx.show(f"!~{rm} #{len(self.items)}")
elif k == 'r':
elif key == 'r':
play = lambda n: ctx.slides(playDuration[1], *map(lambda i: f"!{i}", self.items[-n:]), "!done")
try: blockingAskThen(play, "n", int, str(len(self.items)))
except ValueError: ctx.show("Invalid Count")
elif k == 'k':
elif key == 'k':
def save(answer):
if isinstance(answer, (list, str)): self.items = answer
else: ctx.show(f"Not List: {answer}")
Expand All @@ -100,7 +108,7 @@ def finish(self):
from sys import argv, stdout, stderr
def main(args = argv[1:]):
cfg = app.parse_args(args)
global sfontPreset; sfontPreset = cfg.note_preset
global synth; calmSetSFont(synth, INSTRUMENT_SF2, cfg.note_preset); synth.start()
pygame.mixer.init(sampleRate)
pygame.init()
rkeys = RecordKeys()
Expand All @@ -110,6 +118,7 @@ def main(args = argv[1:]):
if cfg.o == NAME_STDOUT: stdout.write(srt)
else:
with open(cfg.o, "w+", encoding="utf-8") as srtf: srtf.write(srt)
system(cmdOnDone.replace("out", cfg.o))


def gameWindow(caption, dimen):
Expand All @@ -126,25 +135,25 @@ def gameCenterText(text, cx=0.5, cy=0.5):
bg.blit(rtext, textpos)
pygame.display.flip()

def clamlySetFont(synth, path, preset):
def mainloopCall(handler):
for event in pygame.event.get(): handler(event)

def calmSetSFont(synth, path, preset):
try: synth.setFont(path, preset)
except OSError: print(f"{path} is required to enable note playback!", file=stderr)

def guiReadPitches(note_base:int, reducer, onKey = lambda ctx, k: (), caption = "Add Pitches"):
gameWindow(caption, WINDOW_DIMEN)

synth = NoteSynth(sampleRate)
def playSec(n_sec, pitch):
synth.noteSwitch(pitch)
timeout(n_sec, synth.noteoff)

clamlySetFont(synth, INSTRUMENT_SF2, sfontPreset)
synth.start()
playSec(playDuration[1], note_base)

ctx = RefUpdate("Ready~!")
intro = ctx.slides(playDuration[2], f"0={dumpOctave(note_base)}", "[P] proceed",
"[-=] slide pitch", "[R]replay [K]bulk entry", "Have Fun!")
"[-=] slide pitch", "[R]replay [K]list", "Have Fun!")

def baseSlide(n):
nonlocal note_base
Expand All @@ -158,7 +167,7 @@ def defaultOnKey(k):
elif k == '=': baseSlide(+10)
elif k == 'p': raise NonlocalReturn("proceed")
elif k == '\r':
try: reducer.accept(readOctave(ctx.text))
try: reducer.accept(readOctave(ctx.item))
except ValueError: ctx.show(":\\")
else: onKey(ctx, k)
def onEvent(event):
Expand All @@ -180,21 +189,20 @@ def onEvent(event):

while True: #v main logic
if pygame.font and ctx.hasUpdate():
text = ctx.text
text = ctx.item
if len(text) != 0 and text[0] == '!':
cmd = text[1:]
if cmd == "done": synth.noteoff()
elif cmd.startswith("~"): playSec(playDuration[0], int(cmd[1:].rsplit("#")[0]))
else: synth.noteSwitch(int(cmd))
gameCenterText(text)

try:
for event in pygame.event.get(): onEvent(event)
try: mainloopCall(onEvent)
except NonlocalReturn as exc:
if exc.value == "proceed": break
return reducer.finish()

class CallFlagTimed(CallFlag): #< time record (op -- op1)*
class SwitchCallTimed(SwitchCall): #< time record (op -- op1)*
def __init__(self, op, op1):
super().__init__(op, op1)
self.t0 = time()
Expand All @@ -213,13 +221,10 @@ def guiReadTimeline(pitchz, reducer, play = None, play_seek = 0.0, caption = "Ad
gameWindow(caption, WINDOW_DIMEN)
if play != None:
mus.load(play)
let(mus.set_volume, bgmVolume)
mus.play()

synth = NoteSynth(sampleRate)
clamlySetFont(synth, INSTRUMENT_SF2, sfontPreset)
synth.start()

onPausePlay = CallFlagTimed(mus.unpause, mus.pause)
onPausePlay = SwitchCallTimed(mus.unpause, mus.pause)
gameCenterText("[A]keep [S]split")
t0 = time()
t1 = None
Expand Down Expand Up @@ -254,15 +259,18 @@ def giveSegment():

def onEvent(event):
nonlocal t0, t1
if event.type == pygame.KEYDOWN:
key = chr(event.key)
if key == 'a':
evt = event.type
isKdown = (evt == pygame.KEYDOWN)
def actButton(): return ('a' if event.button == 1 else 's')
if isKdown or evt == pygame.MOUSEBUTTONDOWN:
key = chr(event.key) if isKdown else actButton()
if key == 's': giveSegment(); splitNote()
elif key == 'a':
t1 = time()
splitNote()
elif key == 's': giveSegment(); splitNote()
elif event.type == pygame.KEYUP:
key = chr(event.key)
if key == 'a':
elif evt == pygame.KEYUP or evt == pygame.MOUSEBUTTONUP:
key = chr(event.key) if evt==pygame.KEYUP else actButton()
if key == 'a': # paired A
synth.noteoff()
giveSegment()
elif key == ' ':
Expand All @@ -275,11 +283,11 @@ def onEvent(event):
elif key == '=': # volume up
mus.set_volume(mus.get_volume() + d_volume)

elif event.type == pygame.QUIT: raise SystemExit()
elif evt == pygame.QUIT: raise SystemExit()
while True:
try:
for event in pygame.event.get(): onEvent(event)
try: mainloopCall(onEvent)
except NonlocalReturn: break
synth.noteoff()
return reducer.finish()

if __name__ == "__main__": main()
38 changes: 20 additions & 18 deletions hachiko_bapu/hachitools.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from typing import Any, Callable, Optional, TypeVar; T = TypeVar("T")
from typing import Any, Callable, Optional, List, TypeVar, Generic
T = TypeVar("T"); R = TypeVar("R")

from threading import Timer
from os import environ

SEC_MS = 1000

def htmlColor(c:str): return tuple(int(c[i-1:i+1], 16) for i in range(1, len(c), 2))
def grayColor(n:int): return (n,n,n)

def env(name:str, transform:Callable[[str],T], default:T) -> T:
Expand All @@ -20,34 +22,34 @@ def __init__(self, value = None):
@property
def value(self): return self.args[0]

class Fold:
class Fold(Generic[T, R]):
def __init__(self): pass
def accept(self, value): pass
def finish(self): pass
def accept(self, value:T): pass
def finish(self) -> R: pass

class AsList(Fold):
class AsList(Generic[T], Fold[T, List[T]]):
def __init__(self):
self.items = []
def accept(self, value):
self.items.append(value)
def finish(self): return self.items

class RefUpdate:
def __init__(self, initial = ""):
self._text = initial; self.last_text = None
class RefUpdate(Generic[T]):
def __init__(self, initial:T):
self._item = initial; self.last_item:Optional[T] = None
@property
def text(self): return self._text
def update(self): self.last_text = self._text
def item(self): return self._item
def _updated(self): self.last_item = self._item

def show(self, text):
self.update()
self._text = text
def hasUpdate(self):
has_upd = self.last_text != self.text
self.update()
has_upd = self.last_item != self.item
self._updated() # used in check loop
return has_upd
def slides(self, n_sec, *texts):
stream = iter(texts)
def show(self, item:T):
self._updated()
self._item = item
def slides(self, n_sec, *items:T):
stream = iter(items)
def showNext():
nonlocal timeouts
try:
Expand All @@ -57,7 +59,7 @@ def showNext():
timeouts = [timeout(n_sec, showNext)]
return timeouts

class CallFlag:
class SwitchCall:
def __init__(self, op, op1):
self.flag = False
self.op, self.op1 = op, op1
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def parse_requirements(requirements):
return list(filter(lambda s: s.strip() != "", items))

setup(
name="hachiko-bapu", version="0.1.7",
name="hachiko-bapu", version="0.1.8",
python_requires=">=3.5",
author="duangsuse", author_email="fedora-opensuse@outlook.com",
url="https://github.com/duangsuse-valid-projects/Hachiko",
Expand Down

0 comments on commit 679a4a6

Please sign in to comment.