Skip to content

Commit

Permalink
Implemented the tool.
Browse files Browse the repository at this point in the history
  • Loading branch information
LeStahL committed Nov 19, 2023
1 parent 23fc467 commit b88e17f
Show file tree
Hide file tree
Showing 9 changed files with 1,291 additions and 2 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# sointu-executable-msx
Easy-to-use tool to create executable music using sointu.
Easy-to-use tool to create executable music using Sointu.
808 changes: 807 additions & 1 deletion poetry.lock

Large diffs are not rendered by default.

64 changes: 64 additions & 0 deletions pyinstaller.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# -*- mode: python ; coding: utf-8 -*-
from os.path import abspath, join
from zipfile import ZipFile
from platform import system

moduleName = 'sointuexemsx'
rootPath = abspath('.')
buildPath = join(rootPath, 'build')
distPath = join(rootPath, 'dist')
sourcePath = join(rootPath, moduleName)

block_cipher = None

a = Analysis([
join(sourcePath, '__main__.py'),
],
pathex=[],
binaries=[],
datas=[
(join(sourcePath, 'play.asm'), moduleName),
(join(sourcePath, 'wav.asm'), moduleName),
],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='{}'.format(moduleName),
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=True,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=join(rootPath, 'team210.ico'),
)

exeFileName = '{}{}'.format(moduleName, '.exe' if system() == 'Windows' else '')
zipFileName = '{}-{}.zip'.format(moduleName, system())
zipFile = ZipFile(join(distPath, zipFileName), mode='w')
zipFile.write(join(distPath, exeFileName), arcname=join(moduleName, exeFileName))
zipFile.write(join(rootPath, 'LICENSE'), arcname=join(moduleName, 'LICENSE'))
zipFile.write(join(rootPath, 'README.md'), arcname=join(moduleName, 'README.md'))
zipFile.close()
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.11,<3.13"
importlib = "^1.0.4"
argparse = "^1.4.0"
cached-path = "^1.5.0"

[tool.poetry.group.dev.dependencies]
pyinstaller = "^6.2.0"
Expand Down
Empty file added sointuexemsx/__init__.py
Empty file.
182 changes: 182 additions & 0 deletions sointuexemsx/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
from argparse import (
ArgumentParser,
Namespace,
)
from importlib.resources import files
import sointuexemsx
from cached_path import cached_path
from pathlib import Path
from winreg import (
ConnectRegistry,
OpenKey,
HKEY_LOCAL_MACHINE,
HKEYType,
QueryValueEx,
)
from subprocess import (
run,
CompletedProcess,
)
from tempfile import TemporaryDirectory
from os.path import (
basename,
splitext,
exists,
)
from zipfile import ZipFile
from sys import exit

if __name__ == '__main__':
# Parse command line arguments.
parser: ArgumentParser = ArgumentParser("sointu-executable-msx", description="Easy-to-use tool to create executable music using Sointu.")
parser.add_argument(nargs=1, dest='input', help='Track file to compile.')
parser.add_argument('-b,--brutal', dest='brutal', action='store_true', help='Use brutal Crinkler settings (takes much longer, but executable will be smaller).')
parser.add_argument('-n,--nfo', dest='nfo', default=None, help='Add a NFO to the release archive.')
parser.add_argument('-d,--delay', dest='delay', default=0, type=int, help='Add a delay in ms before starting to play the track (useful for computationally heavy tracks).')
args: Namespace = parser.parse_args()

# Check argument sanity
if args.input is None or type(args.input) != list or len(args.input) != 1:
print('Input argument missing or wrong format:', args.input)
exit(1)

if not exists(args.input[0]):
print("Input file does not exist:", args.input[0])
exit(1)

if args.nfo is not None and not exists(args.nfo):
print("NFO file does not exist:", args.nfo)

# Download dependencies.
crinkler: Path = cached_path(
url_or_filename='https://github.com/runestubbe/Crinkler/releases/download/v2.3/crinkler23.zip!crinkler23/Win64/Crinkler.exe',
extract_archive=True,
)
nasm: Path = cached_path(
url_or_filename='https://www.nasm.us/pub/nasm/releasebuilds/2.16.01/win64/nasm-2.16.01-win64.zip!nasm-2.16.01/nasm.exe',
extract_archive=True,
)
sointu: Path = cached_path(
'https://github.com/vsariola/sointu/releases/download/v0.3.0/sointu-Windows.zip!sointu-windows/sointu-compile.exe',
extract_archive=True,
)

# Find Windows SDK path.
registry: HKEYType = ConnectRegistry(None, HKEY_LOCAL_MACHINE)
windowsSdkKey: HKEYType = OpenKey(registry, r'SOFTWARE\WOW6432Node\Microsoft\Microsoft SDKs\Windows\v10.0')
windowsSdkProductVersion, _ = QueryValueEx(windowsSdkKey, r'ProductVersion')
windowsSdkInstallFolder, _ = QueryValueEx(windowsSdkKey, r'InstallationFolder')
windowsSdkKey.Close()
registry.Close()
windowsSdkLibPath: Path = Path(windowsSdkInstallFolder) / 'Lib' / '{}.0'.format(windowsSdkProductVersion) / 'um' / 'x86'

# Determine track base name without extension.
base, _ = splitext(basename(args.input[0]))

# Run sointu-compile on the track.
with Path(TemporaryDirectory().name) as outputDirectory:
print('Exporting to:', outputDirectory)

# Run sointu-compile to convert the track to assembly.
result: CompletedProcess = run([
sointu,
'-arch', '386',
'-e', 'asm,inc',
'-o', outputDirectory / 'music.asm',
args.input[0],
])

if result.returncode != 0:
print("Could not compile track with Sointu.")
else:
print("Compiled sointu track.")

# Assemble the wav writer.
result: CompletedProcess = run([
nasm,
'-f', 'win32',
'-I', outputDirectory,
files(sointuexemsx) / 'wav.asm',
'-DTRACK_INCLUDE="{}"'.format(outputDirectory / 'music.inc'),
'-DFILENAME="{}"'.format('music.wav'),
'-o', outputDirectory / 'wav.obj',
])

if result.returncode != 0:
print("Could not assemble wav writer.")
else:
print("Assembled wav writer.")

# Assemble the player.
result: CompletedProcess = run([
nasm,
'-f', 'win32',
'-I', outputDirectory,
files(sointuexemsx) / 'play.asm',
'-DTRACK_INCLUDE="{}"'.format(outputDirectory / 'music.inc'),
'-DDELAY' if args.delay != 0 else '',
'-DDELAY_MS={}'.format(args.delay) if args.delay != 0 else '',
'-o', outputDirectory / 'play.obj',
])

if result.returncode != 0:
print("Could not assemble player.")
else:
print("Assembled player.")

# Assemble the track.
result: CompletedProcess = run([
nasm,
'-f', 'win32',
'-I', outputDirectory,
outputDirectory / 'music.asm',
'-o', outputDirectory/ 'music.obj',
])

if result.returncode != 0:
print("Could not assemble track.")
else:
print("Assembled track.")

# Link wav writer.
# Note: When using the list based api, quotes in arguments
# are not escaped properly.
result: CompletedProcess = run(' '.join(map(str,[
crinkler,
'/LIBPATH:"{}"'.format(outputDirectory),
'/LIBPATH:"{}"'.format(windowsSdkLibPath),
outputDirectory / 'wav.obj',
outputDirectory / 'music.obj',
'/OUT:{}'.format(outputDirectory / '{}-wav.exe'.format(base)),
'Winmm.lib',
'Kernel32.lib',
'User32.lib',
'/COMPMODE:VERYSLOW' if args.brutal else '/COMPMODE:FAST',
])), shell=True)

# Link player.
# Note: When using the list based api, quotes in arguments
# are not escaped properly.
result: CompletedProcess = run(' '.join(map(str,[
crinkler,
'/LIBPATH:"{}"'.format(outputDirectory),
'/LIBPATH:"{}"'.format(windowsSdkLibPath),
outputDirectory / 'play.obj',
outputDirectory / 'music.obj',
'/OUT:{}'.format(outputDirectory / '{}-play.exe'.format(base)),
'Winmm.lib',
'Kernel32.lib',
'User32.lib',
'/COMPMODE:VERYSLOW' if args.brutal else '/COMPMODE:FAST',
])), shell=True)

# Create release archive.
zipFile: ZipFile = ZipFile('{}.zip'.format(base), 'w')
zipFile.write(filename=str(outputDirectory / '{}-wav.exe'.format(base)), arcname='{}/{}-wav.exe'.format(base, base))
zipFile.write(filename=str(outputDirectory / '{}-play.exe'.format(base)), arcname='{}/{}-play.exe'.format(base, base))
if args.nfo is not None:
nfoBaseWithExt = basename(args.nfo)
zipFile.write(filename=args.nfo, arcname='{}/{}'.format(base, nfoBaseWithExt))
zipFile.close()

exit(0)
129 changes: 129 additions & 0 deletions sointuexemsx/play.asm
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
%define MANGLED
%include TRACK_INCLUDE

%define WAVE_FORMAT_PCM 0x1
%define WAVE_FORMAT_IEEE_FLOAT 0x3
%define WHDR_PREPARED 0x2
%define WAVE_MAPPER 0xFFFFFFFF
%define TIME_SAMPLES 0x2
%define PM_REMOVE 0x1

section .bss
sound_buffer:
resb SU_LENGTH_IN_SAMPLES * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT

wave_out_handle:
resd 1

msg:
resd 1
message:
resd 7

section .data
wave_format:
%ifdef SU_SAMPLE_FLOAT
dw WAVE_FORMAT_IEEE_FLOAT
%else ; SU_SAMPLE_FLOAT
dw WAVE_FORMAT_PCM
%endif ; SU_SAMPLE_FLOAT
dw SU_CHANNEL_COUNT
dd SU_SAMPLE_RATE
dd SU_SAMPLE_SIZE * SU_SAMPLE_RATE * SU_CHANNEL_COUNT
dw SU_SAMPLE_SIZE * SU_CHANNEL_COUNT
dw SU_SAMPLE_SIZE * 8
dw 0

wave_header:
dd sound_buffer
dd SU_LENGTH_IN_SAMPLES * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT
times 2 dd 0
dd WHDR_PREPARED
times 4 dd 0
wave_header_end:

mmtime:
dd TIME_SAMPLES
sample:
times 2 dd 0
mmtime_end:

section .text
symbols:
extern _CreateThread@24
extern _waveOutOpen@24
extern _waveOutWrite@12
extern _waveOutGetPosition@12
extern _PeekMessageA@20
extern _TranslateMessage@4
extern _DispatchMessageA@4
%ifdef DELAY
extern _Sleep@4
%endif ; DELAY

global _mainCRTStartup
_mainCRTStartup:
; win32 uses the cdecl calling convention. This is more readable imo ;)
; We can also skip the prologue; Windows doesn't mind.

%ifdef SU_LOAD_GMDLS
call _su_load_gmdls
%endif ; SU_LOAD_GMDLS

times 2 push 0
push sound_buffer
lea eax, _su_render_song@4
push eax
times 2 push 0
call _CreateThread@24

%ifdef DELAY
; We can't start playing too early or the missing samples will be audible.
push DELAY_MS
call _Sleep@4
%endif ; DELAY

; We render in the background while playing already. Fortunately,
; Windows is slow with the calls below, so we're not worried that
; we don't have enough samples ready before the track starts.
times 3 push 0
push wave_format
push WAVE_MAPPER
push wave_out_handle
call _waveOutOpen@24

push wave_header_end - wave_header
push wave_header
push dword [wave_out_handle]
call _waveOutWrite@12

; We need to handle windows messages properly while playing, as waveOutWrite is async.
mainloop:
dispatchloop:
push PM_REMOVE
times 3 push 0
push msg
call _PeekMessageA@20
jz dispatchloop_end

push msg
call _TranslateMessage@4

push msg
call _DispatchMessageA@4

jmp dispatchloop
dispatchloop_end:

push mmtime_end - mmtime
push mmtime
push dword [wave_out_handle]
call _waveOutGetPosition@12

cmp dword [sample], SU_LENGTH_IN_SAMPLES
jne mainloop

exit:
; At least we can skip the epilogue :)
leave
ret
Loading

0 comments on commit b88e17f

Please sign in to comment.