mirror of
https://github.com/mrkite/soundsmith.git
synced 2024-08-31 17:28:58 +00:00
initial import
This commit is contained in:
commit
4f1a6bb8c4
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
*.map
|
||||
*.swp
|
||||
songs/
|
24
LICENSE
Normal file
24
LICENSE
Normal file
@ -0,0 +1,24 @@
|
||||
Copyright (c) 2017, Sean Kasun
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
||||
THE POSSIBILITY OF SUCH DAMAGE.
|
19
Makefile
Normal file
19
Makefile
Normal file
@ -0,0 +1,19 @@
|
||||
TS = src/es5503.ts src/handle.ts
|
||||
SSTS = src/smith.ts src/player.ts
|
||||
FTATS = src/fta.ts src/ftaplayer.ts
|
||||
|
||||
.DELETE_ON_ERROR:
|
||||
|
||||
all: smith.js fta.js
|
||||
|
||||
smith.js: $(TS) $(SSTS)
|
||||
tsc
|
||||
|
||||
fta.js: $(TS) $(FTATS)
|
||||
tsc -p tsconfig_fta.json
|
||||
|
||||
check:
|
||||
tslint $(TS) $(FTATS) $(SSTS)
|
||||
|
||||
clean:
|
||||
rm -f smith.js fta.js
|
18
README.md
Normal file
18
README.md
Normal file
@ -0,0 +1,18 @@
|
||||
# Javascript Soundsmith Player
|
||||
|
||||
Soundsmith was a music program released in the late 80s for the Apple IIgs. It
|
||||
was used by many games and demos for their music. This is an implementation of
|
||||
the Soundsmith player written in Typescript.
|
||||
|
||||
You can check out a live demo [here](https://seancode.com/soundsmith).
|
||||
|
||||
`src/` contains the sourcecode for the player.
|
||||
|
||||
`extract/` contains a bunch of commandline tools I wrote to help extract music
|
||||
from demos and other software.
|
||||
|
||||
`docs/` contains documentation on how Soundsmith works, as well as documentation
|
||||
on how I extracted music from the trickier sources.
|
||||
|
||||
I've also included a second special music player that can play older FTA music, like the
|
||||
sort found in the Nucleus demo.
|
857
docs/modulae.html
Normal file
857
docs/modulae.html
Normal file
@ -0,0 +1,857 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
|
||||
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="application/xhtml+xml; charset=UTF-8" />
|
||||
<meta name="generator" content="AsciiDoc 8.6.9" />
|
||||
<title>Extracting music from Modulae</title>
|
||||
<style type="text/css">
|
||||
/* Shared CSS for AsciiDoc xhtml11 and html5 backends */
|
||||
|
||||
/* Default font. */
|
||||
body {
|
||||
font-family: Georgia,serif;
|
||||
}
|
||||
|
||||
/* Title font. */
|
||||
h1, h2, h3, h4, h5, h6,
|
||||
div.title, caption.title,
|
||||
thead, p.table.header,
|
||||
#toctitle,
|
||||
#author, #revnumber, #revdate, #revremark,
|
||||
#footer {
|
||||
font-family: Arial,Helvetica,sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 1em 5% 1em 5%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:visited {
|
||||
color: fuchsia;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: italic;
|
||||
color: navy;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
color: #083194;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: #527bbd;
|
||||
margin-top: 1.2em;
|
||||
margin-bottom: 0.5em;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
border-bottom: 2px solid silver;
|
||||
}
|
||||
h2 {
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
h3 {
|
||||
float: left;
|
||||
}
|
||||
h3 + * {
|
||||
clear: left;
|
||||
}
|
||||
h5 {
|
||||
font-size: 1.0em;
|
||||
}
|
||||
|
||||
div.sectionbody {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 1px solid silver;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
ul, ol, li > p {
|
||||
margin-top: 0;
|
||||
}
|
||||
ul > li { color: #aaa; }
|
||||
ul > li > * { color: black; }
|
||||
|
||||
.monospaced, code, pre {
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
font-size: inherit;
|
||||
color: navy;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
#author {
|
||||
color: #527bbd;
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
#email {
|
||||
}
|
||||
#revnumber, #revdate, #revremark {
|
||||
}
|
||||
|
||||
#footer {
|
||||
font-size: small;
|
||||
border-top: 2px solid silver;
|
||||
padding-top: 0.5em;
|
||||
margin-top: 4.0em;
|
||||
}
|
||||
#footer-text {
|
||||
float: left;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
#footer-badges {
|
||||
float: right;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
|
||||
#preamble {
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
div.imageblock, div.exampleblock, div.verseblock,
|
||||
div.quoteblock, div.literalblock, div.listingblock, div.sidebarblock,
|
||||
div.admonitionblock {
|
||||
margin-top: 1.0em;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
div.admonitionblock {
|
||||
margin-top: 2.0em;
|
||||
margin-bottom: 2.0em;
|
||||
margin-right: 10%;
|
||||
color: #606060;
|
||||
}
|
||||
|
||||
div.content { /* Block element content. */
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Block element titles. */
|
||||
div.title, caption.title {
|
||||
color: #527bbd;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
margin-top: 1.0em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
div.title + * {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
td div.title:first-child {
|
||||
margin-top: 0.0em;
|
||||
}
|
||||
div.content div.title:first-child {
|
||||
margin-top: 0.0em;
|
||||
}
|
||||
div.content + div.title {
|
||||
margin-top: 0.0em;
|
||||
}
|
||||
|
||||
div.sidebarblock > div.content {
|
||||
background: #ffffee;
|
||||
border: 1px solid #dddddd;
|
||||
border-left: 4px solid #f0f0f0;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
div.listingblock > div.content {
|
||||
border: 1px solid #dddddd;
|
||||
border-left: 5px solid #f0f0f0;
|
||||
background: #f8f8f8;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
div.quoteblock, div.verseblock {
|
||||
padding-left: 1.0em;
|
||||
margin-left: 1.0em;
|
||||
margin-right: 10%;
|
||||
border-left: 5px solid #f0f0f0;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
div.quoteblock > div.attribution {
|
||||
padding-top: 0.5em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
div.verseblock > pre.content {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
div.verseblock > div.attribution {
|
||||
padding-top: 0.75em;
|
||||
text-align: left;
|
||||
}
|
||||
/* DEPRECATED: Pre version 8.2.7 verse style literal block. */
|
||||
div.verseblock + div.attribution {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
div.admonitionblock .icon {
|
||||
vertical-align: top;
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
text-decoration: underline;
|
||||
color: #527bbd;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
div.admonitionblock td.content {
|
||||
padding-left: 0.5em;
|
||||
border-left: 3px solid #dddddd;
|
||||
}
|
||||
|
||||
div.exampleblock > div.content {
|
||||
border-left: 3px solid #dddddd;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
div.imageblock div.content { padding-left: 0; }
|
||||
span.image img { border-style: none; vertical-align: text-bottom; }
|
||||
a.image:visited { color: white; }
|
||||
|
||||
dl {
|
||||
margin-top: 0.8em;
|
||||
margin-bottom: 0.8em;
|
||||
}
|
||||
dt {
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0;
|
||||
font-style: normal;
|
||||
color: navy;
|
||||
}
|
||||
dd > *:first-child {
|
||||
margin-top: 0.1em;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
list-style-position: outside;
|
||||
}
|
||||
ol.arabic {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
ol.loweralpha {
|
||||
list-style-type: lower-alpha;
|
||||
}
|
||||
ol.upperalpha {
|
||||
list-style-type: upper-alpha;
|
||||
}
|
||||
ol.lowerroman {
|
||||
list-style-type: lower-roman;
|
||||
}
|
||||
ol.upperroman {
|
||||
list-style-type: upper-roman;
|
||||
}
|
||||
|
||||
div.compact ul, div.compact ol,
|
||||
div.compact p, div.compact p,
|
||||
div.compact div, div.compact div {
|
||||
margin-top: 0.1em;
|
||||
margin-bottom: 0.1em;
|
||||
}
|
||||
|
||||
tfoot {
|
||||
font-weight: bold;
|
||||
}
|
||||
td > div.verse {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
div.hdlist {
|
||||
margin-top: 0.8em;
|
||||
margin-bottom: 0.8em;
|
||||
}
|
||||
div.hdlist tr {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
dt.hdlist1.strong, td.hdlist1.strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
td.hdlist1 {
|
||||
vertical-align: top;
|
||||
font-style: normal;
|
||||
padding-right: 0.8em;
|
||||
color: navy;
|
||||
}
|
||||
td.hdlist2 {
|
||||
vertical-align: top;
|
||||
}
|
||||
div.hdlist.compact tr {
|
||||
margin: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.comment {
|
||||
background: yellow;
|
||||
}
|
||||
|
||||
.footnote, .footnoteref {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
span.footnote, span.footnoteref {
|
||||
vertical-align: super;
|
||||
}
|
||||
|
||||
#footnotes {
|
||||
margin: 20px 0 20px 0;
|
||||
padding: 7px 0 0 0;
|
||||
}
|
||||
|
||||
#footnotes div.footnote {
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
|
||||
#footnotes hr {
|
||||
border: none;
|
||||
border-top: 1px solid silver;
|
||||
height: 1px;
|
||||
text-align: left;
|
||||
margin-left: 0;
|
||||
width: 20%;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
div.colist td {
|
||||
padding-right: 0.5em;
|
||||
padding-bottom: 0.3em;
|
||||
vertical-align: top;
|
||||
}
|
||||
div.colist td img {
|
||||
margin-top: 0.3em;
|
||||
}
|
||||
|
||||
@media print {
|
||||
#footer-badges { display: none; }
|
||||
}
|
||||
|
||||
#toc {
|
||||
margin-bottom: 2.5em;
|
||||
}
|
||||
|
||||
#toctitle {
|
||||
color: #527bbd;
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
margin-top: 1.0em;
|
||||
margin-bottom: 0.1em;
|
||||
}
|
||||
|
||||
div.toclevel0, div.toclevel1, div.toclevel2, div.toclevel3, div.toclevel4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
div.toclevel2 {
|
||||
margin-left: 2em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
div.toclevel3 {
|
||||
margin-left: 4em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
div.toclevel4 {
|
||||
margin-left: 6em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
span.aqua { color: aqua; }
|
||||
span.black { color: black; }
|
||||
span.blue { color: blue; }
|
||||
span.fuchsia { color: fuchsia; }
|
||||
span.gray { color: gray; }
|
||||
span.green { color: green; }
|
||||
span.lime { color: lime; }
|
||||
span.maroon { color: maroon; }
|
||||
span.navy { color: navy; }
|
||||
span.olive { color: olive; }
|
||||
span.purple { color: purple; }
|
||||
span.red { color: red; }
|
||||
span.silver { color: silver; }
|
||||
span.teal { color: teal; }
|
||||
span.white { color: white; }
|
||||
span.yellow { color: yellow; }
|
||||
|
||||
span.aqua-background { background: aqua; }
|
||||
span.black-background { background: black; }
|
||||
span.blue-background { background: blue; }
|
||||
span.fuchsia-background { background: fuchsia; }
|
||||
span.gray-background { background: gray; }
|
||||
span.green-background { background: green; }
|
||||
span.lime-background { background: lime; }
|
||||
span.maroon-background { background: maroon; }
|
||||
span.navy-background { background: navy; }
|
||||
span.olive-background { background: olive; }
|
||||
span.purple-background { background: purple; }
|
||||
span.red-background { background: red; }
|
||||
span.silver-background { background: silver; }
|
||||
span.teal-background { background: teal; }
|
||||
span.white-background { background: white; }
|
||||
span.yellow-background { background: yellow; }
|
||||
|
||||
span.big { font-size: 2em; }
|
||||
span.small { font-size: 0.6em; }
|
||||
|
||||
span.underline { text-decoration: underline; }
|
||||
span.overline { text-decoration: overline; }
|
||||
span.line-through { text-decoration: line-through; }
|
||||
|
||||
div.unbreakable { page-break-inside: avoid; }
|
||||
|
||||
|
||||
/*
|
||||
* xhtml11 specific
|
||||
*
|
||||
* */
|
||||
|
||||
div.tableblock {
|
||||
margin-top: 1.0em;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
div.tableblock > table {
|
||||
border: 3px solid #527bbd;
|
||||
}
|
||||
thead, p.table.header {
|
||||
font-weight: bold;
|
||||
color: #527bbd;
|
||||
}
|
||||
p.table {
|
||||
margin-top: 0;
|
||||
}
|
||||
/* Because the table frame attribute is overriden by CSS in most browsers. */
|
||||
div.tableblock > table[frame="void"] {
|
||||
border-style: none;
|
||||
}
|
||||
div.tableblock > table[frame="hsides"] {
|
||||
border-left-style: none;
|
||||
border-right-style: none;
|
||||
}
|
||||
div.tableblock > table[frame="vsides"] {
|
||||
border-top-style: none;
|
||||
border-bottom-style: none;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* html5 specific
|
||||
*
|
||||
* */
|
||||
|
||||
table.tableblock {
|
||||
margin-top: 1.0em;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
thead, p.tableblock.header {
|
||||
font-weight: bold;
|
||||
color: #527bbd;
|
||||
}
|
||||
p.tableblock {
|
||||
margin-top: 0;
|
||||
}
|
||||
table.tableblock {
|
||||
border-width: 3px;
|
||||
border-spacing: 0px;
|
||||
border-style: solid;
|
||||
border-color: #527bbd;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
th.tableblock, td.tableblock {
|
||||
border-width: 1px;
|
||||
padding: 4px;
|
||||
border-style: solid;
|
||||
border-color: #527bbd;
|
||||
}
|
||||
|
||||
table.tableblock.frame-topbot {
|
||||
border-left-style: hidden;
|
||||
border-right-style: hidden;
|
||||
}
|
||||
table.tableblock.frame-sides {
|
||||
border-top-style: hidden;
|
||||
border-bottom-style: hidden;
|
||||
}
|
||||
table.tableblock.frame-none {
|
||||
border-style: hidden;
|
||||
}
|
||||
|
||||
th.tableblock.halign-left, td.tableblock.halign-left {
|
||||
text-align: left;
|
||||
}
|
||||
th.tableblock.halign-center, td.tableblock.halign-center {
|
||||
text-align: center;
|
||||
}
|
||||
th.tableblock.halign-right, td.tableblock.halign-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
th.tableblock.valign-top, td.tableblock.valign-top {
|
||||
vertical-align: top;
|
||||
}
|
||||
th.tableblock.valign-middle, td.tableblock.valign-middle {
|
||||
vertical-align: middle;
|
||||
}
|
||||
th.tableblock.valign-bottom, td.tableblock.valign-bottom {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* manpage specific
|
||||
*
|
||||
* */
|
||||
|
||||
body.manpage h1 {
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
border-top: 2px solid silver;
|
||||
border-bottom: 2px solid silver;
|
||||
}
|
||||
body.manpage h2 {
|
||||
border-style: none;
|
||||
}
|
||||
body.manpage div.sectionbody {
|
||||
margin-left: 3em;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body.manpage div#toc { display: none; }
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
/*<![CDATA[*/
|
||||
var asciidoc = { // Namespace.
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// Table Of Contents generator
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
/* Author: Mihai Bazon, September 2002
|
||||
* http://students.infoiasi.ro/~mishoo
|
||||
*
|
||||
* Table Of Content generator
|
||||
* Version: 0.4
|
||||
*
|
||||
* Feel free to use this script under the terms of the GNU General Public
|
||||
* License, as long as you do not remove or alter this notice.
|
||||
*/
|
||||
|
||||
/* modified by Troy D. Hanson, September 2006. License: GPL */
|
||||
/* modified by Stuart Rackham, 2006, 2009. License: GPL */
|
||||
|
||||
// toclevels = 1..4.
|
||||
toc: function (toclevels) {
|
||||
|
||||
function getText(el) {
|
||||
var text = "";
|
||||
for (var i = el.firstChild; i != null; i = i.nextSibling) {
|
||||
if (i.nodeType == 3 /* Node.TEXT_NODE */) // IE doesn't speak constants.
|
||||
text += i.data;
|
||||
else if (i.firstChild != null)
|
||||
text += getText(i);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function TocEntry(el, text, toclevel) {
|
||||
this.element = el;
|
||||
this.text = text;
|
||||
this.toclevel = toclevel;
|
||||
}
|
||||
|
||||
function tocEntries(el, toclevels) {
|
||||
var result = new Array;
|
||||
var re = new RegExp('[hH]([1-'+(toclevels+1)+'])');
|
||||
// Function that scans the DOM tree for header elements (the DOM2
|
||||
// nodeIterator API would be a better technique but not supported by all
|
||||
// browsers).
|
||||
var iterate = function (el) {
|
||||
for (var i = el.firstChild; i != null; i = i.nextSibling) {
|
||||
if (i.nodeType == 1 /* Node.ELEMENT_NODE */) {
|
||||
var mo = re.exec(i.tagName);
|
||||
if (mo && (i.getAttribute("class") || i.getAttribute("className")) != "float") {
|
||||
result[result.length] = new TocEntry(i, getText(i), mo[1]-1);
|
||||
}
|
||||
iterate(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
iterate(el);
|
||||
return result;
|
||||
}
|
||||
|
||||
var toc = document.getElementById("toc");
|
||||
if (!toc) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete existing TOC entries in case we're reloading the TOC.
|
||||
var tocEntriesToRemove = [];
|
||||
var i;
|
||||
for (i = 0; i < toc.childNodes.length; i++) {
|
||||
var entry = toc.childNodes[i];
|
||||
if (entry.nodeName.toLowerCase() == 'div'
|
||||
&& entry.getAttribute("class")
|
||||
&& entry.getAttribute("class").match(/^toclevel/))
|
||||
tocEntriesToRemove.push(entry);
|
||||
}
|
||||
for (i = 0; i < tocEntriesToRemove.length; i++) {
|
||||
toc.removeChild(tocEntriesToRemove[i]);
|
||||
}
|
||||
|
||||
// Rebuild TOC entries.
|
||||
var entries = tocEntries(document.getElementById("content"), toclevels);
|
||||
for (var i = 0; i < entries.length; ++i) {
|
||||
var entry = entries[i];
|
||||
if (entry.element.id == "")
|
||||
entry.element.id = "_toc_" + i;
|
||||
var a = document.createElement("a");
|
||||
a.href = "#" + entry.element.id;
|
||||
a.appendChild(document.createTextNode(entry.text));
|
||||
var div = document.createElement("div");
|
||||
div.appendChild(a);
|
||||
div.className = "toclevel" + entry.toclevel;
|
||||
toc.appendChild(div);
|
||||
}
|
||||
if (entries.length == 0)
|
||||
toc.parentNode.removeChild(toc);
|
||||
},
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// Footnotes generator
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
/* Based on footnote generation code from:
|
||||
* http://www.brandspankingnew.net/archive/2005/07/format_footnote.html
|
||||
*/
|
||||
|
||||
footnotes: function () {
|
||||
// Delete existing footnote entries in case we're reloading the footnodes.
|
||||
var i;
|
||||
var noteholder = document.getElementById("footnotes");
|
||||
if (!noteholder) {
|
||||
return;
|
||||
}
|
||||
var entriesToRemove = [];
|
||||
for (i = 0; i < noteholder.childNodes.length; i++) {
|
||||
var entry = noteholder.childNodes[i];
|
||||
if (entry.nodeName.toLowerCase() == 'div' && entry.getAttribute("class") == "footnote")
|
||||
entriesToRemove.push(entry);
|
||||
}
|
||||
for (i = 0; i < entriesToRemove.length; i++) {
|
||||
noteholder.removeChild(entriesToRemove[i]);
|
||||
}
|
||||
|
||||
// Rebuild footnote entries.
|
||||
var cont = document.getElementById("content");
|
||||
var spans = cont.getElementsByTagName("span");
|
||||
var refs = {};
|
||||
var n = 0;
|
||||
for (i=0; i<spans.length; i++) {
|
||||
if (spans[i].className == "footnote") {
|
||||
n++;
|
||||
var note = spans[i].getAttribute("data-note");
|
||||
if (!note) {
|
||||
// Use [\s\S] in place of . so multi-line matches work.
|
||||
// Because JavaScript has no s (dotall) regex flag.
|
||||
note = spans[i].innerHTML.match(/\s*\[([\s\S]*)]\s*/)[1];
|
||||
spans[i].innerHTML =
|
||||
"[<a id='_footnoteref_" + n + "' href='#_footnote_" + n +
|
||||
"' title='View footnote' class='footnote'>" + n + "</a>]";
|
||||
spans[i].setAttribute("data-note", note);
|
||||
}
|
||||
noteholder.innerHTML +=
|
||||
"<div class='footnote' id='_footnote_" + n + "'>" +
|
||||
"<a href='#_footnoteref_" + n + "' title='Return to text'>" +
|
||||
n + "</a>. " + note + "</div>";
|
||||
var id =spans[i].getAttribute("id");
|
||||
if (id != null) refs["#"+id] = n;
|
||||
}
|
||||
}
|
||||
if (n == 0)
|
||||
noteholder.parentNode.removeChild(noteholder);
|
||||
else {
|
||||
// Process footnoterefs.
|
||||
for (i=0; i<spans.length; i++) {
|
||||
if (spans[i].className == "footnoteref") {
|
||||
var href = spans[i].getElementsByTagName("a")[0].getAttribute("href");
|
||||
href = href.match(/#.*/)[0]; // Because IE return full URL.
|
||||
n = refs[href];
|
||||
spans[i].innerHTML =
|
||||
"[<a href='#_footnote_" + n +
|
||||
"' title='View footnote' class='footnote'>" + n + "</a>]";
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
install: function(toclevels) {
|
||||
var timerId;
|
||||
|
||||
function reinstall() {
|
||||
asciidoc.footnotes();
|
||||
if (toclevels) {
|
||||
asciidoc.toc(toclevels);
|
||||
}
|
||||
}
|
||||
|
||||
function reinstallAndRemoveTimer() {
|
||||
clearInterval(timerId);
|
||||
reinstall();
|
||||
}
|
||||
|
||||
timerId = setInterval(reinstall, 500);
|
||||
if (document.addEventListener)
|
||||
document.addEventListener("DOMContentLoaded", reinstallAndRemoveTimer, false);
|
||||
else
|
||||
window.onload = reinstallAndRemoveTimer;
|
||||
}
|
||||
|
||||
}
|
||||
asciidoc.install();
|
||||
/*]]>*/
|
||||
</script>
|
||||
</head>
|
||||
<body class="article">
|
||||
<div id="header">
|
||||
<h1>Extracting music from Modulae</h1>
|
||||
</div>
|
||||
<div id="content">
|
||||
<div id="preamble">
|
||||
<div class="sectionbody">
|
||||
<div class="paragraph"><p>This is a walkthrough for how I located and extracted the music from FTA’s
|
||||
Modulae demo, straight from the 2mg disk image.</p></div>
|
||||
<div class="paragraph"><p>We’ll begin by disassembling the boot block. The boot block is the first
|
||||
block on disk, and is always loaded into <code>$0800</code>.</p></div>
|
||||
<div class="listingblock">
|
||||
<div class="content"><!-- Generator: GNU source-highlight 3.1.8
|
||||
by Lorenzo Bettini
|
||||
http://www.lorenzobettini.it
|
||||
http://www.gnu.org/software/src-highlite -->
|
||||
<pre><tt><span style="color: #990000">.</span>/disasm modulae<span style="color: #990000">.</span>2mg <span style="color: #993399">800</span> <span style="color: #993399">0</span> <span style="color: #993399">1</span> <span style="color: #990000">></span> boot<span style="color: #990000">.</span>s</tt></pre></div></div>
|
||||
<div class="paragraph"><p>This command says to extract one 512-byte block starting at block 0, into memory
|
||||
location <code>$800</code> and disassemble it.</p></div>
|
||||
<div class="paragraph"><p>If we analyze the disassembled boot block, we will see that the function at
|
||||
<code>$08f9</code> loads a block from disk. Following that backward, we discover that
|
||||
at <code>$08c9</code>, it loads blocks 7—13 into RAM starting at <code>$9400</code>. Then it
|
||||
calls a function at <code>$0a00</code> and passes it the address of the RAM it just loaded.</p></div>
|
||||
<div class="paragraph"><p>Blocks 7—13 contain the main loader, it’s a routine responsible for
|
||||
loading the rest of the disk into various parts of RAM. The call to
|
||||
<code>$0a00</code> decrunches the loader. To help with that, I built a tool that can
|
||||
properly decrunch those blocks.</p></div>
|
||||
<div class="listingblock">
|
||||
<div class="content"><!-- Generator: GNU source-highlight 3.1.8
|
||||
by Lorenzo Bettini
|
||||
http://www.lorenzobettini.it
|
||||
http://www.gnu.org/software/src-highlite -->
|
||||
<pre><tt><span style="color: #990000">.</span>/decrunch modulae<span style="color: #990000">.</span>2mg <span style="color: #993399">7</span> <span style="color: #993399">6</span> loader</tt></pre></div></div>
|
||||
<div class="paragraph"><p>This basically extracts 6 blocks from the disk image, starting at block 7. It
|
||||
decrunches them, and saves the result into a file called <code>loader</code>.</p></div>
|
||||
<div class="paragraph"><p>Now we can disassemble the loader.</p></div>
|
||||
<div class="listingblock">
|
||||
<div class="content"><!-- Generator: GNU source-highlight 3.1.8
|
||||
by Lorenzo Bettini
|
||||
http://www.lorenzobettini.it
|
||||
http://www.gnu.org/software/src-highlite -->
|
||||
<pre><tt><span style="color: #990000">.</span>/disasm loader <span style="color: #993399">9400</span> <span style="color: #990000">></span> loader<span style="color: #990000">.</span>s</tt></pre></div></div>
|
||||
<div class="paragraph"><p>The <code>disasm</code> command will disassemble the entire file if the offset and size
|
||||
arguements are omitted. It also will work on a byte-level if the file passed
|
||||
isn’t a 2mg disk image.</p></div>
|
||||
<div class="paragraph"><p>We remember from the previous disassembly that these blocks should be
|
||||
loaded into RAM at <code>$9400</code>, so we pass that address to <code>disasm</code>.</p></div>
|
||||
<div class="paragraph"><p>Now we inspect the loader disassembly. After a bit of inspection, we can
|
||||
see that the main loading is done at <code>$9ad6</code>. It uses a table of addresses
|
||||
located at <code>$9bad</code> to determine which blocks to load and where in RAM to put
|
||||
them. It then later runs the decruncher (still at <code>$0a00</code>) on various blocks
|
||||
of RAM.</p></div>
|
||||
<div class="paragraph"><p>This table is the thing we care about. The first word is the starting
|
||||
block on disk, which is followed by a bunch of 8-byte records. The first
|
||||
dword of each record is the target address, the second dword is the number
|
||||
of 256-byte pages to load. It loads these sequentially from the disk,
|
||||
beginning at the starting block. It ends when a record is 0 pages long.</p></div>
|
||||
<div class="paragraph"><p>I built a <code>dumptbl</code> tool that makes it easier to parse this table into
|
||||
human readable format, telling us which blocks to load and where. We can
|
||||
then choose to inspect the various blocks, one by one and determine which
|
||||
ones we care about.</p></div>
|
||||
<div class="paragraph"><p>The table we want to dump is at <code>$9bad</code>, we need to calculate that
|
||||
position relative to the start of the file to determine its disk offset.</p></div>
|
||||
<div class="listingblock">
|
||||
<div class="content">
|
||||
<pre><code>$9bad - $9400 = $07ad</code></pre>
|
||||
</div></div>
|
||||
<div class="paragraph"><p>Thus, we call <code>dumptbl</code> with the following:</p></div>
|
||||
<div class="listingblock">
|
||||
<div class="content"><!-- Generator: GNU source-highlight 3.1.8
|
||||
by Lorenzo Bettini
|
||||
http://www.lorenzobettini.it
|
||||
http://www.gnu.org/software/src-highlite -->
|
||||
<pre><tt><span style="color: #990000">.</span>/dumptbl loader 7ad</tt></pre></div></div>
|
||||
<div class="paragraph"><p>(Note how we pass <code>loader</code> which is the unpacked binary, and not
|
||||
<code>loader.s</code> which is the disassembly).</p></div>
|
||||
<div class="paragraph"><p>Using this table, we can inspect various blocks.</p></div>
|
||||
<div class="paragraph"><p>The very first entry in the table is actually interesting. It consists of
|
||||
58 blocks, starting at block 16 on disk, uncrunched. If we look at this
|
||||
data, it’s actually the “And now, FTA Presents…” raw audio data.
|
||||
It’s stored as an unsigned 8-bit PCM data. It’s in mono, and should
|
||||
be played back at 11,025 Hz. I went ahead and slapped a <code>.wav</code> header
|
||||
onto the audio data to save it for posterity. You’ll find it in the
|
||||
songs/modulae folder.</p></div>
|
||||
<div class="paragraph"><p>I also found a music player. Starting at block 801, 27 blocks are loaded
|
||||
into <code>$1000</code>.</p></div>
|
||||
<div class="paragraph"><p>The music player uses a wavebank loaded into <code>$9:0000</code>, and music data at
|
||||
<code>$a:0400</code>. Sure enough, we can find those blocks in the table used by the main
|
||||
loader. The music data is crunched, the sound data isn’t.</p></div>
|
||||
<div class="listingblock">
|
||||
<div class="content"><!-- Generator: GNU source-highlight 3.1.8
|
||||
by Lorenzo Bettini
|
||||
http://www.lorenzobettini.it
|
||||
http://www.gnu.org/software/src-highlite -->
|
||||
<pre><tt><span style="color: #990000">.</span>/decrunch modulae<span style="color: #990000">.</span>2mg <span style="color: #993399">828</span> <span style="color: #993399">130</span> intro<span style="color: #990000">.</span>wb raw
|
||||
<span style="color: #990000">.</span>/decrunch modulae<span style="color: #990000">.</span>2mg <span style="color: #993399">958</span> <span style="color: #993399">5</span> intro<span style="color: #990000">.</span>song</tt></pre></div></div>
|
||||
<div class="paragraph"><p>Now, there should technically be two different music files in Modulae. One
|
||||
for the intro, and the other for the main demo. The music player doesn’t
|
||||
seem to reference them, so I’m guessing a <strong>second</strong> music player is actually loaded
|
||||
at another point. Instead of hunting for it, I decide to check the entries
|
||||
of the main loader. Sure enough, I find another song and wavebank.</p></div>
|
||||
<div class="listingblock">
|
||||
<div class="content"><!-- Generator: GNU source-highlight 3.1.8
|
||||
by Lorenzo Bettini
|
||||
http://www.lorenzobettini.it
|
||||
http://www.gnu.org/software/src-highlite -->
|
||||
<pre><tt><span style="color: #990000">.</span>/decrunch modulae<span style="color: #990000">.</span>2mg <span style="color: #993399">189</span> <span style="color: #993399">133</span> demo<span style="color: #990000">.</span>wb raw
|
||||
<span style="color: #990000">.</span>/decrunch modulae<span style="color: #990000">.</span>2mg <span style="color: #993399">162</span> <span style="color: #993399">27</span> demo<span style="color: #990000">.</span>song</tt></pre></div></div>
|
||||
<div class="paragraph"><p>And thus we have both songs extracted. The final step is to trim off the
|
||||
excess padding. Since the songs are padded to block boundaries, they have
|
||||
useless extra padding at the end. <code>trimwb</code> and <code>trimmusic</code> will calculate the
|
||||
proper length of the wavebank and song files and output trimmed down versions.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="footnotes"><hr /></div>
|
||||
<div id="footer">
|
||||
<div id="footer-text">
|
||||
Last updated
|
||||
2017-08-21 10:48:15 MST
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
122
docs/modulae.txt
Normal file
122
docs/modulae.txt
Normal file
@ -0,0 +1,122 @@
|
||||
Extracting music from Modulae
|
||||
=============================
|
||||
|
||||
This is a walkthrough for how I located and extracted the music from FTA's
|
||||
Modulae demo, straight from the 2mg disk image.
|
||||
|
||||
We'll begin by disassembling the boot block. The boot block is the first
|
||||
block on disk, and is always loaded into `$0800`.
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
./disasm modulae.2mg 800 0 1 > boot.s
|
||||
----
|
||||
|
||||
This command says to extract one 512-byte block starting at block 0, into memory
|
||||
location `$800` and disassemble it.
|
||||
|
||||
If we analyze the disassembled boot block, we will see that the function at
|
||||
`$08f9` loads a block from disk. Following that backward, we discover that
|
||||
at `$08c9`, it loads blocks 7--13 into RAM starting at `$9400`. Then it
|
||||
calls a function at `$0a00` and passes it the address of the RAM it just loaded.
|
||||
|
||||
Blocks 7--13 contain the main loader, it's a routine responsible for
|
||||
loading the rest of the disk into various parts of RAM. The call to
|
||||
`$0a00` decrunches the loader. To help with that, I built a tool that can
|
||||
properly decrunch those blocks.
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
./decrunch modulae.2mg 7 6 loader
|
||||
----
|
||||
|
||||
This basically extracts 6 blocks from the disk image, starting at block 7. It
|
||||
decrunches them, and saves the result into a file called `loader`.
|
||||
|
||||
Now we can disassemble the loader.
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
./disasm loader 9400 > loader.s
|
||||
----
|
||||
|
||||
The `disasm` command will disassemble the entire file if the offset and size
|
||||
arguements are omitted. It also will work on a byte-level if the file passed
|
||||
isn't a 2mg disk image.
|
||||
|
||||
We remember from the previous disassembly that these blocks should be
|
||||
loaded into RAM at `$9400`, so we pass that address to `disasm`.
|
||||
|
||||
Now we inspect the loader disassembly. After a bit of inspection, we can
|
||||
see that the main loading is done at `$9ad6`. It uses a table of addresses
|
||||
located at `$9bad` to determine which blocks to load and where in RAM to put
|
||||
them. It then later runs the decruncher (still at `$0a00`) on various blocks
|
||||
of RAM.
|
||||
|
||||
This table is the thing we care about. The first word is the starting
|
||||
block on disk, which is followed by a bunch of 8-byte records. The first
|
||||
dword of each record is the target address, the second dword is the number
|
||||
of 256-byte pages to load. It loads these sequentially from the disk,
|
||||
beginning at the starting block. It ends when a record is 0 pages long.
|
||||
|
||||
I built a `dumptbl` tool that makes it easier to parse this table into
|
||||
human readable format, telling us which blocks to load and where. We can
|
||||
then choose to inspect the various blocks, one by one and determine which
|
||||
ones we care about.
|
||||
|
||||
The table we want to dump is at `$9bad`, we need to calculate that
|
||||
position relative to the start of the file to determine its disk offset.
|
||||
|
||||
----
|
||||
$9bad - $9400 = $07ad
|
||||
----
|
||||
|
||||
Thus, we call `dumptbl` with the following:
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
./dumptbl loader 7ad
|
||||
----
|
||||
|
||||
(Note how we pass `loader` which is the unpacked binary, and not
|
||||
`loader.s` which is the disassembly).
|
||||
|
||||
Using this table, we can inspect various blocks.
|
||||
|
||||
The very first entry in the table is actually interesting. It consists of
|
||||
58 blocks, starting at block 16 on disk, uncrunched. If we look at this
|
||||
data, it's actually the ``And now, FTA Presents...'' raw audio data.
|
||||
It's stored as an unsigned 8-bit PCM data. It's in mono, and should
|
||||
be played back at 11,025 Hz. I went ahead and slapped a `.wav` header
|
||||
onto the audio data to save it for posterity. You'll find it in the
|
||||
songs/modulae folder.
|
||||
|
||||
I also found a music player. Starting at block 801, 27 blocks are loaded
|
||||
into `$1000`.
|
||||
|
||||
The music player uses a wavebank loaded into `$9:0000`, and music data at
|
||||
`$a:0400`. Sure enough, we can find those blocks in the table used by the main
|
||||
loader. The music data is crunched, the sound data isn't.
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
./decrunch modulae.2mg 828 130 intro.wb raw
|
||||
./decrunch modulae.2mg 958 5 intro.song
|
||||
----
|
||||
|
||||
Now, there should technically be two different music files in Modulae. One
|
||||
for the intro, and the other for the main demo. The music player doesn't
|
||||
seem to reference them, so I'm guessing a *second* music player is actually loaded
|
||||
at another point. Instead of hunting for it, I decide to check the entries
|
||||
of the main loader. Sure enough, I find another song and wavebank.
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
./decrunch modulae.2mg 189 133 demo.wb raw
|
||||
./decrunch modulae.2mg 162 27 demo.song
|
||||
----
|
||||
|
||||
And thus we have both songs extracted. The final step is to trim off the
|
||||
excess padding. Since the songs are padded to block boundaries, they have
|
||||
useless extra padding at the end. `trimwb` and `trimmusic` will calculate the
|
||||
proper length of the wavebank and song files and output trimmed down versions.
|
838
docs/nucleus.html
Normal file
838
docs/nucleus.html
Normal file
@ -0,0 +1,838 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
|
||||
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="application/xhtml+xml; charset=UTF-8" />
|
||||
<meta name="generator" content="AsciiDoc 8.6.9" />
|
||||
<title>Nucleus Demo</title>
|
||||
<style type="text/css">
|
||||
/* Shared CSS for AsciiDoc xhtml11 and html5 backends */
|
||||
|
||||
/* Default font. */
|
||||
body {
|
||||
font-family: Georgia,serif;
|
||||
}
|
||||
|
||||
/* Title font. */
|
||||
h1, h2, h3, h4, h5, h6,
|
||||
div.title, caption.title,
|
||||
thead, p.table.header,
|
||||
#toctitle,
|
||||
#author, #revnumber, #revdate, #revremark,
|
||||
#footer {
|
||||
font-family: Arial,Helvetica,sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 1em 5% 1em 5%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:visited {
|
||||
color: fuchsia;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: italic;
|
||||
color: navy;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
color: #083194;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: #527bbd;
|
||||
margin-top: 1.2em;
|
||||
margin-bottom: 0.5em;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
border-bottom: 2px solid silver;
|
||||
}
|
||||
h2 {
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
h3 {
|
||||
float: left;
|
||||
}
|
||||
h3 + * {
|
||||
clear: left;
|
||||
}
|
||||
h5 {
|
||||
font-size: 1.0em;
|
||||
}
|
||||
|
||||
div.sectionbody {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 1px solid silver;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
ul, ol, li > p {
|
||||
margin-top: 0;
|
||||
}
|
||||
ul > li { color: #aaa; }
|
||||
ul > li > * { color: black; }
|
||||
|
||||
.monospaced, code, pre {
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
font-size: inherit;
|
||||
color: navy;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
#author {
|
||||
color: #527bbd;
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
#email {
|
||||
}
|
||||
#revnumber, #revdate, #revremark {
|
||||
}
|
||||
|
||||
#footer {
|
||||
font-size: small;
|
||||
border-top: 2px solid silver;
|
||||
padding-top: 0.5em;
|
||||
margin-top: 4.0em;
|
||||
}
|
||||
#footer-text {
|
||||
float: left;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
#footer-badges {
|
||||
float: right;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
|
||||
#preamble {
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
div.imageblock, div.exampleblock, div.verseblock,
|
||||
div.quoteblock, div.literalblock, div.listingblock, div.sidebarblock,
|
||||
div.admonitionblock {
|
||||
margin-top: 1.0em;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
div.admonitionblock {
|
||||
margin-top: 2.0em;
|
||||
margin-bottom: 2.0em;
|
||||
margin-right: 10%;
|
||||
color: #606060;
|
||||
}
|
||||
|
||||
div.content { /* Block element content. */
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Block element titles. */
|
||||
div.title, caption.title {
|
||||
color: #527bbd;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
margin-top: 1.0em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
div.title + * {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
td div.title:first-child {
|
||||
margin-top: 0.0em;
|
||||
}
|
||||
div.content div.title:first-child {
|
||||
margin-top: 0.0em;
|
||||
}
|
||||
div.content + div.title {
|
||||
margin-top: 0.0em;
|
||||
}
|
||||
|
||||
div.sidebarblock > div.content {
|
||||
background: #ffffee;
|
||||
border: 1px solid #dddddd;
|
||||
border-left: 4px solid #f0f0f0;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
div.listingblock > div.content {
|
||||
border: 1px solid #dddddd;
|
||||
border-left: 5px solid #f0f0f0;
|
||||
background: #f8f8f8;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
div.quoteblock, div.verseblock {
|
||||
padding-left: 1.0em;
|
||||
margin-left: 1.0em;
|
||||
margin-right: 10%;
|
||||
border-left: 5px solid #f0f0f0;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
div.quoteblock > div.attribution {
|
||||
padding-top: 0.5em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
div.verseblock > pre.content {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
div.verseblock > div.attribution {
|
||||
padding-top: 0.75em;
|
||||
text-align: left;
|
||||
}
|
||||
/* DEPRECATED: Pre version 8.2.7 verse style literal block. */
|
||||
div.verseblock + div.attribution {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
div.admonitionblock .icon {
|
||||
vertical-align: top;
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
text-decoration: underline;
|
||||
color: #527bbd;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
div.admonitionblock td.content {
|
||||
padding-left: 0.5em;
|
||||
border-left: 3px solid #dddddd;
|
||||
}
|
||||
|
||||
div.exampleblock > div.content {
|
||||
border-left: 3px solid #dddddd;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
div.imageblock div.content { padding-left: 0; }
|
||||
span.image img { border-style: none; vertical-align: text-bottom; }
|
||||
a.image:visited { color: white; }
|
||||
|
||||
dl {
|
||||
margin-top: 0.8em;
|
||||
margin-bottom: 0.8em;
|
||||
}
|
||||
dt {
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0;
|
||||
font-style: normal;
|
||||
color: navy;
|
||||
}
|
||||
dd > *:first-child {
|
||||
margin-top: 0.1em;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
list-style-position: outside;
|
||||
}
|
||||
ol.arabic {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
ol.loweralpha {
|
||||
list-style-type: lower-alpha;
|
||||
}
|
||||
ol.upperalpha {
|
||||
list-style-type: upper-alpha;
|
||||
}
|
||||
ol.lowerroman {
|
||||
list-style-type: lower-roman;
|
||||
}
|
||||
ol.upperroman {
|
||||
list-style-type: upper-roman;
|
||||
}
|
||||
|
||||
div.compact ul, div.compact ol,
|
||||
div.compact p, div.compact p,
|
||||
div.compact div, div.compact div {
|
||||
margin-top: 0.1em;
|
||||
margin-bottom: 0.1em;
|
||||
}
|
||||
|
||||
tfoot {
|
||||
font-weight: bold;
|
||||
}
|
||||
td > div.verse {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
div.hdlist {
|
||||
margin-top: 0.8em;
|
||||
margin-bottom: 0.8em;
|
||||
}
|
||||
div.hdlist tr {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
dt.hdlist1.strong, td.hdlist1.strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
td.hdlist1 {
|
||||
vertical-align: top;
|
||||
font-style: normal;
|
||||
padding-right: 0.8em;
|
||||
color: navy;
|
||||
}
|
||||
td.hdlist2 {
|
||||
vertical-align: top;
|
||||
}
|
||||
div.hdlist.compact tr {
|
||||
margin: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.comment {
|
||||
background: yellow;
|
||||
}
|
||||
|
||||
.footnote, .footnoteref {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
span.footnote, span.footnoteref {
|
||||
vertical-align: super;
|
||||
}
|
||||
|
||||
#footnotes {
|
||||
margin: 20px 0 20px 0;
|
||||
padding: 7px 0 0 0;
|
||||
}
|
||||
|
||||
#footnotes div.footnote {
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
|
||||
#footnotes hr {
|
||||
border: none;
|
||||
border-top: 1px solid silver;
|
||||
height: 1px;
|
||||
text-align: left;
|
||||
margin-left: 0;
|
||||
width: 20%;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
div.colist td {
|
||||
padding-right: 0.5em;
|
||||
padding-bottom: 0.3em;
|
||||
vertical-align: top;
|
||||
}
|
||||
div.colist td img {
|
||||
margin-top: 0.3em;
|
||||
}
|
||||
|
||||
@media print {
|
||||
#footer-badges { display: none; }
|
||||
}
|
||||
|
||||
#toc {
|
||||
margin-bottom: 2.5em;
|
||||
}
|
||||
|
||||
#toctitle {
|
||||
color: #527bbd;
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
margin-top: 1.0em;
|
||||
margin-bottom: 0.1em;
|
||||
}
|
||||
|
||||
div.toclevel0, div.toclevel1, div.toclevel2, div.toclevel3, div.toclevel4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
div.toclevel2 {
|
||||
margin-left: 2em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
div.toclevel3 {
|
||||
margin-left: 4em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
div.toclevel4 {
|
||||
margin-left: 6em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
span.aqua { color: aqua; }
|
||||
span.black { color: black; }
|
||||
span.blue { color: blue; }
|
||||
span.fuchsia { color: fuchsia; }
|
||||
span.gray { color: gray; }
|
||||
span.green { color: green; }
|
||||
span.lime { color: lime; }
|
||||
span.maroon { color: maroon; }
|
||||
span.navy { color: navy; }
|
||||
span.olive { color: olive; }
|
||||
span.purple { color: purple; }
|
||||
span.red { color: red; }
|
||||
span.silver { color: silver; }
|
||||
span.teal { color: teal; }
|
||||
span.white { color: white; }
|
||||
span.yellow { color: yellow; }
|
||||
|
||||
span.aqua-background { background: aqua; }
|
||||
span.black-background { background: black; }
|
||||
span.blue-background { background: blue; }
|
||||
span.fuchsia-background { background: fuchsia; }
|
||||
span.gray-background { background: gray; }
|
||||
span.green-background { background: green; }
|
||||
span.lime-background { background: lime; }
|
||||
span.maroon-background { background: maroon; }
|
||||
span.navy-background { background: navy; }
|
||||
span.olive-background { background: olive; }
|
||||
span.purple-background { background: purple; }
|
||||
span.red-background { background: red; }
|
||||
span.silver-background { background: silver; }
|
||||
span.teal-background { background: teal; }
|
||||
span.white-background { background: white; }
|
||||
span.yellow-background { background: yellow; }
|
||||
|
||||
span.big { font-size: 2em; }
|
||||
span.small { font-size: 0.6em; }
|
||||
|
||||
span.underline { text-decoration: underline; }
|
||||
span.overline { text-decoration: overline; }
|
||||
span.line-through { text-decoration: line-through; }
|
||||
|
||||
div.unbreakable { page-break-inside: avoid; }
|
||||
|
||||
|
||||
/*
|
||||
* xhtml11 specific
|
||||
*
|
||||
* */
|
||||
|
||||
div.tableblock {
|
||||
margin-top: 1.0em;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
div.tableblock > table {
|
||||
border: 3px solid #527bbd;
|
||||
}
|
||||
thead, p.table.header {
|
||||
font-weight: bold;
|
||||
color: #527bbd;
|
||||
}
|
||||
p.table {
|
||||
margin-top: 0;
|
||||
}
|
||||
/* Because the table frame attribute is overriden by CSS in most browsers. */
|
||||
div.tableblock > table[frame="void"] {
|
||||
border-style: none;
|
||||
}
|
||||
div.tableblock > table[frame="hsides"] {
|
||||
border-left-style: none;
|
||||
border-right-style: none;
|
||||
}
|
||||
div.tableblock > table[frame="vsides"] {
|
||||
border-top-style: none;
|
||||
border-bottom-style: none;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* html5 specific
|
||||
*
|
||||
* */
|
||||
|
||||
table.tableblock {
|
||||
margin-top: 1.0em;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
thead, p.tableblock.header {
|
||||
font-weight: bold;
|
||||
color: #527bbd;
|
||||
}
|
||||
p.tableblock {
|
||||
margin-top: 0;
|
||||
}
|
||||
table.tableblock {
|
||||
border-width: 3px;
|
||||
border-spacing: 0px;
|
||||
border-style: solid;
|
||||
border-color: #527bbd;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
th.tableblock, td.tableblock {
|
||||
border-width: 1px;
|
||||
padding: 4px;
|
||||
border-style: solid;
|
||||
border-color: #527bbd;
|
||||
}
|
||||
|
||||
table.tableblock.frame-topbot {
|
||||
border-left-style: hidden;
|
||||
border-right-style: hidden;
|
||||
}
|
||||
table.tableblock.frame-sides {
|
||||
border-top-style: hidden;
|
||||
border-bottom-style: hidden;
|
||||
}
|
||||
table.tableblock.frame-none {
|
||||
border-style: hidden;
|
||||
}
|
||||
|
||||
th.tableblock.halign-left, td.tableblock.halign-left {
|
||||
text-align: left;
|
||||
}
|
||||
th.tableblock.halign-center, td.tableblock.halign-center {
|
||||
text-align: center;
|
||||
}
|
||||
th.tableblock.halign-right, td.tableblock.halign-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
th.tableblock.valign-top, td.tableblock.valign-top {
|
||||
vertical-align: top;
|
||||
}
|
||||
th.tableblock.valign-middle, td.tableblock.valign-middle {
|
||||
vertical-align: middle;
|
||||
}
|
||||
th.tableblock.valign-bottom, td.tableblock.valign-bottom {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* manpage specific
|
||||
*
|
||||
* */
|
||||
|
||||
body.manpage h1 {
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
border-top: 2px solid silver;
|
||||
border-bottom: 2px solid silver;
|
||||
}
|
||||
body.manpage h2 {
|
||||
border-style: none;
|
||||
}
|
||||
body.manpage div.sectionbody {
|
||||
margin-left: 3em;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body.manpage div#toc { display: none; }
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
/*<![CDATA[*/
|
||||
var asciidoc = { // Namespace.
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// Table Of Contents generator
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
/* Author: Mihai Bazon, September 2002
|
||||
* http://students.infoiasi.ro/~mishoo
|
||||
*
|
||||
* Table Of Content generator
|
||||
* Version: 0.4
|
||||
*
|
||||
* Feel free to use this script under the terms of the GNU General Public
|
||||
* License, as long as you do not remove or alter this notice.
|
||||
*/
|
||||
|
||||
/* modified by Troy D. Hanson, September 2006. License: GPL */
|
||||
/* modified by Stuart Rackham, 2006, 2009. License: GPL */
|
||||
|
||||
// toclevels = 1..4.
|
||||
toc: function (toclevels) {
|
||||
|
||||
function getText(el) {
|
||||
var text = "";
|
||||
for (var i = el.firstChild; i != null; i = i.nextSibling) {
|
||||
if (i.nodeType == 3 /* Node.TEXT_NODE */) // IE doesn't speak constants.
|
||||
text += i.data;
|
||||
else if (i.firstChild != null)
|
||||
text += getText(i);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function TocEntry(el, text, toclevel) {
|
||||
this.element = el;
|
||||
this.text = text;
|
||||
this.toclevel = toclevel;
|
||||
}
|
||||
|
||||
function tocEntries(el, toclevels) {
|
||||
var result = new Array;
|
||||
var re = new RegExp('[hH]([1-'+(toclevels+1)+'])');
|
||||
// Function that scans the DOM tree for header elements (the DOM2
|
||||
// nodeIterator API would be a better technique but not supported by all
|
||||
// browsers).
|
||||
var iterate = function (el) {
|
||||
for (var i = el.firstChild; i != null; i = i.nextSibling) {
|
||||
if (i.nodeType == 1 /* Node.ELEMENT_NODE */) {
|
||||
var mo = re.exec(i.tagName);
|
||||
if (mo && (i.getAttribute("class") || i.getAttribute("className")) != "float") {
|
||||
result[result.length] = new TocEntry(i, getText(i), mo[1]-1);
|
||||
}
|
||||
iterate(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
iterate(el);
|
||||
return result;
|
||||
}
|
||||
|
||||
var toc = document.getElementById("toc");
|
||||
if (!toc) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete existing TOC entries in case we're reloading the TOC.
|
||||
var tocEntriesToRemove = [];
|
||||
var i;
|
||||
for (i = 0; i < toc.childNodes.length; i++) {
|
||||
var entry = toc.childNodes[i];
|
||||
if (entry.nodeName.toLowerCase() == 'div'
|
||||
&& entry.getAttribute("class")
|
||||
&& entry.getAttribute("class").match(/^toclevel/))
|
||||
tocEntriesToRemove.push(entry);
|
||||
}
|
||||
for (i = 0; i < tocEntriesToRemove.length; i++) {
|
||||
toc.removeChild(tocEntriesToRemove[i]);
|
||||
}
|
||||
|
||||
// Rebuild TOC entries.
|
||||
var entries = tocEntries(document.getElementById("content"), toclevels);
|
||||
for (var i = 0; i < entries.length; ++i) {
|
||||
var entry = entries[i];
|
||||
if (entry.element.id == "")
|
||||
entry.element.id = "_toc_" + i;
|
||||
var a = document.createElement("a");
|
||||
a.href = "#" + entry.element.id;
|
||||
a.appendChild(document.createTextNode(entry.text));
|
||||
var div = document.createElement("div");
|
||||
div.appendChild(a);
|
||||
div.className = "toclevel" + entry.toclevel;
|
||||
toc.appendChild(div);
|
||||
}
|
||||
if (entries.length == 0)
|
||||
toc.parentNode.removeChild(toc);
|
||||
},
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// Footnotes generator
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
/* Based on footnote generation code from:
|
||||
* http://www.brandspankingnew.net/archive/2005/07/format_footnote.html
|
||||
*/
|
||||
|
||||
footnotes: function () {
|
||||
// Delete existing footnote entries in case we're reloading the footnodes.
|
||||
var i;
|
||||
var noteholder = document.getElementById("footnotes");
|
||||
if (!noteholder) {
|
||||
return;
|
||||
}
|
||||
var entriesToRemove = [];
|
||||
for (i = 0; i < noteholder.childNodes.length; i++) {
|
||||
var entry = noteholder.childNodes[i];
|
||||
if (entry.nodeName.toLowerCase() == 'div' && entry.getAttribute("class") == "footnote")
|
||||
entriesToRemove.push(entry);
|
||||
}
|
||||
for (i = 0; i < entriesToRemove.length; i++) {
|
||||
noteholder.removeChild(entriesToRemove[i]);
|
||||
}
|
||||
|
||||
// Rebuild footnote entries.
|
||||
var cont = document.getElementById("content");
|
||||
var spans = cont.getElementsByTagName("span");
|
||||
var refs = {};
|
||||
var n = 0;
|
||||
for (i=0; i<spans.length; i++) {
|
||||
if (spans[i].className == "footnote") {
|
||||
n++;
|
||||
var note = spans[i].getAttribute("data-note");
|
||||
if (!note) {
|
||||
// Use [\s\S] in place of . so multi-line matches work.
|
||||
// Because JavaScript has no s (dotall) regex flag.
|
||||
note = spans[i].innerHTML.match(/\s*\[([\s\S]*)]\s*/)[1];
|
||||
spans[i].innerHTML =
|
||||
"[<a id='_footnoteref_" + n + "' href='#_footnote_" + n +
|
||||
"' title='View footnote' class='footnote'>" + n + "</a>]";
|
||||
spans[i].setAttribute("data-note", note);
|
||||
}
|
||||
noteholder.innerHTML +=
|
||||
"<div class='footnote' id='_footnote_" + n + "'>" +
|
||||
"<a href='#_footnoteref_" + n + "' title='Return to text'>" +
|
||||
n + "</a>. " + note + "</div>";
|
||||
var id =spans[i].getAttribute("id");
|
||||
if (id != null) refs["#"+id] = n;
|
||||
}
|
||||
}
|
||||
if (n == 0)
|
||||
noteholder.parentNode.removeChild(noteholder);
|
||||
else {
|
||||
// Process footnoterefs.
|
||||
for (i=0; i<spans.length; i++) {
|
||||
if (spans[i].className == "footnoteref") {
|
||||
var href = spans[i].getElementsByTagName("a")[0].getAttribute("href");
|
||||
href = href.match(/#.*/)[0]; // Because IE return full URL.
|
||||
n = refs[href];
|
||||
spans[i].innerHTML =
|
||||
"[<a href='#_footnote_" + n +
|
||||
"' title='View footnote' class='footnote'>" + n + "</a>]";
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
install: function(toclevels) {
|
||||
var timerId;
|
||||
|
||||
function reinstall() {
|
||||
asciidoc.footnotes();
|
||||
if (toclevels) {
|
||||
asciidoc.toc(toclevels);
|
||||
}
|
||||
}
|
||||
|
||||
function reinstallAndRemoveTimer() {
|
||||
clearInterval(timerId);
|
||||
reinstall();
|
||||
}
|
||||
|
||||
timerId = setInterval(reinstall, 500);
|
||||
if (document.addEventListener)
|
||||
document.addEventListener("DOMContentLoaded", reinstallAndRemoveTimer, false);
|
||||
else
|
||||
window.onload = reinstallAndRemoveTimer;
|
||||
}
|
||||
|
||||
}
|
||||
asciidoc.install();
|
||||
/*]]>*/
|
||||
</script>
|
||||
</head>
|
||||
<body class="article">
|
||||
<div id="header">
|
||||
<h1>Nucleus Demo</h1>
|
||||
</div>
|
||||
<div id="content">
|
||||
<div id="preamble">
|
||||
<div class="sectionbody">
|
||||
<div class="paragraph"><p>Unfortunately, the Nucleus demo and Photonix tool do not use the Soundsmith
|
||||
format for their music. Instead they use a proprietary format. I decided to
|
||||
reverse engineer this format as well and make a player for them as well.</p></div>
|
||||
<div class="paragraph"><p>We, of course, start with loading the boot block:</p></div>
|
||||
<div class="listingblock">
|
||||
<div class="content"><!-- Generator: GNU source-highlight 3.1.8
|
||||
by Lorenzo Bettini
|
||||
http://www.lorenzobettini.it
|
||||
http://www.gnu.org/software/src-highlite -->
|
||||
<pre><tt><span style="color: #990000">.</span>/disasm nucleus<span style="color: #990000">.</span>2mg <span style="color: #993399">800</span> <span style="color: #993399">0</span> <span style="color: #993399">1</span> <span style="color: #990000">></span> boot<span style="color: #990000">.</span>s</tt></pre></div></div>
|
||||
<div class="paragraph"><p>Which lets us discover the loader is in blocks 7—b.</p></div>
|
||||
<div class="listingblock">
|
||||
<div class="content"><!-- Generator: GNU source-highlight 3.1.8
|
||||
by Lorenzo Bettini
|
||||
http://www.lorenzobettini.it
|
||||
http://www.gnu.org/software/src-highlite -->
|
||||
<pre><tt><span style="color: #990000">.</span>/disasm nucleus<span style="color: #990000">.</span>2mg <span style="color: #993399">9600</span> <span style="color: #993399">7</span> <span style="color: #993399">4</span> <span style="color: #990000">></span> loader<span style="color: #990000">.</span>s</tt></pre></div></div>
|
||||
<div class="paragraph"><p>The loader uses a table starting at <code>$9607</code>, where the first word is the
|
||||
starting block, the second word is the number of blocks, followed by a dword
|
||||
loading address. It repeats until the starting block has the high bit
|
||||
set. Nucleus does not use any compression for its data.</p></div>
|
||||
<div class="paragraph"><p>Using this, we discover the music player, which will let us discover where all
|
||||
the music data and wavebanks are located. (It will also be the thing I study
|
||||
the most in order to write a player).</p></div>
|
||||
<div class="listingblock">
|
||||
<div class="content"><!-- Generator: GNU source-highlight 3.1.8
|
||||
by Lorenzo Bettini
|
||||
http://www.lorenzobettini.it
|
||||
http://www.gnu.org/software/src-highlite -->
|
||||
<pre><tt><span style="color: #990000">.</span>/disasm nucleus<span style="color: #990000">.</span>2mg <span style="color: #993399">81000</span> <span style="color: #993399">333</span> <span style="color: #993399">4</span> <span style="color: #990000">></span> player<span style="color: #990000">.</span>s</tt></pre></div></div>
|
||||
<div class="paragraph"><p>The sound player is more akin to MIDI than a tracker. The songs are broken into
|
||||
channels, each channel controlling 4 oscillators. There aren’t any patterns,
|
||||
there’s just a long list of note frequencies and note lengths for each channel.
|
||||
This means each channel has its own play head, only advancing to the next note
|
||||
when the note length elapses. Each channel loops independently as well.</p></div>
|
||||
<div class="paragraph"><p>The first time the player initializes, it loads a wavebank from <code>$4/0000</code> into
|
||||
sound RAM. Then all future initializations load a wavebank from <code>$3/0000</code>.
|
||||
This means we have two wavebanks, one for the intro song, and the other for all
|
||||
the main songs in the demo. I use the table from the loader to determine which
|
||||
blocks are loaded for each memory location.</p></div>
|
||||
<div class="listingblock">
|
||||
<div class="content"><!-- Generator: GNU source-highlight 3.1.8
|
||||
by Lorenzo Bettini
|
||||
http://www.lorenzobettini.it
|
||||
http://www.gnu.org/software/src-highlite -->
|
||||
<pre><tt><span style="color: #990000">.</span>/decrunch nucleus<span style="color: #990000">.</span>2mg <span style="color: #993399">140</span> <span style="color: #993399">128</span> intro<span style="color: #990000">.</span>wb raw
|
||||
<span style="color: #990000">.</span>/decrunch nucleus<span style="color: #990000">.</span>2mg <span style="color: #993399">12</span> <span style="color: #993399">128</span> main<span style="color: #990000">.</span>wb raw</tt></pre></div></div>
|
||||
<div class="paragraph"><p>The player uses a block of data located at <code>$8/0300</code> to determine all sorts of
|
||||
information about the songs and their channels. I’ll be calling it <code>songdefs</code>.</p></div>
|
||||
<div class="listingblock">
|
||||
<div class="content"><!-- Generator: GNU source-highlight 3.1.8
|
||||
by Lorenzo Bettini
|
||||
http://www.lorenzobettini.it
|
||||
http://www.gnu.org/software/src-highlite -->
|
||||
<pre><tt><span style="color: #990000">.</span>/decrunch nucleus<span style="color: #990000">.</span>2mg <span style="color: #993399">268</span> <span style="color: #993399">2</span> songdefs raw</tt></pre></div></div>
|
||||
<div class="paragraph"><p>This file is divided into chunks. Each chunk is 256 bytes long, and contains
|
||||
information about a song. So the first chunk contains information about the
|
||||
intro song. The second chunk contains information about the first main song,
|
||||
and so on. Using this we can determine there is 1 intro song, and 3 main songs.</p></div>
|
||||
<div class="paragraph"><p>Each chunk contains instrument data for each channel, as well as where in memory
|
||||
to find the note data for that song, as well has the playback speed.</p></div>
|
||||
<div class="paragraph"><p>The word at offset <code>$44</code> in the chunk determines where the note
|
||||
data for that specific song is located, inside bank 8. Using this we can
|
||||
extract the note data for all the songs.</p></div>
|
||||
<div class="listingblock">
|
||||
<div class="content"><!-- Generator: GNU source-highlight 3.1.8
|
||||
by Lorenzo Bettini
|
||||
http://www.lorenzobettini.it
|
||||
http://www.gnu.org/software/src-highlite -->
|
||||
<pre><tt><span style="color: #990000">.</span>/decrunch nucleus<span style="color: #990000">.</span>2mg <span style="color: #993399">284</span> <span style="color: #993399">8</span> intro<span style="color: #990000">.</span>song raw
|
||||
<span style="color: #990000">.</span>/decrunch nucleus<span style="color: #990000">.</span>2mg <span style="color: #993399">270</span> <span style="color: #993399">14</span> main1<span style="color: #990000">.</span>song raw
|
||||
<span style="color: #990000">.</span>/decrunch nucleus<span style="color: #990000">.</span>2mg <span style="color: #993399">292</span> <span style="color: #993399">7</span> main2<span style="color: #990000">.</span>song raw
|
||||
<span style="color: #990000">.</span>/decrunch nucleus<span style="color: #990000">.</span>2mg <span style="color: #993399">299</span> <span style="color: #993399">2</span> main3<span style="color: #990000">.</span>song raw</tt></pre></div></div>
|
||||
<div class="paragraph"><p>I’ll also divide up the songdefs file to provide easy access to each song’s
|
||||
instrument data.</p></div>
|
||||
<div class="listingblock">
|
||||
<div class="content"><!-- Generator: GNU source-highlight 3.1.8
|
||||
by Lorenzo Bettini
|
||||
http://www.lorenzobettini.it
|
||||
http://www.gnu.org/software/src-highlite -->
|
||||
<pre><tt>dd <span style="font-weight: bold"><span style="color: #0000FF">if</span></span><span style="color: #990000">=</span>songdefs <span style="color: #009900">bs</span><span style="color: #990000">=</span><span style="color: #993399">256</span> <span style="color: #009900">skip</span><span style="color: #990000">=</span><span style="color: #993399">0</span> <span style="color: #009900">count</span><span style="color: #990000">=</span><span style="color: #993399">1</span> <span style="color: #009900">of</span><span style="color: #990000">=</span>intro<span style="color: #990000">.</span>inst
|
||||
dd <span style="font-weight: bold"><span style="color: #0000FF">if</span></span><span style="color: #990000">=</span>songdefs <span style="color: #009900">bs</span><span style="color: #990000">=</span><span style="color: #993399">256</span> <span style="color: #009900">skip</span><span style="color: #990000">=</span><span style="color: #993399">1</span> <span style="color: #009900">count</span><span style="color: #990000">=</span><span style="color: #993399">1</span> <span style="color: #009900">of</span><span style="color: #990000">=</span>main1<span style="color: #990000">.</span>inst
|
||||
dd <span style="font-weight: bold"><span style="color: #0000FF">if</span></span><span style="color: #990000">=</span>songdefs <span style="color: #009900">bs</span><span style="color: #990000">=</span><span style="color: #993399">256</span> <span style="color: #009900">skip</span><span style="color: #990000">=</span><span style="color: #993399">2</span> <span style="color: #009900">count</span><span style="color: #990000">=</span><span style="color: #993399">1</span> <span style="color: #009900">of</span><span style="color: #990000">=</span>main2<span style="color: #990000">.</span>inst
|
||||
dd <span style="font-weight: bold"><span style="color: #0000FF">if</span></span><span style="color: #990000">=</span>songdefs <span style="color: #009900">bs</span><span style="color: #990000">=</span><span style="color: #993399">256</span> <span style="color: #009900">skip</span><span style="color: #990000">=</span><span style="color: #993399">3</span> <span style="color: #009900">count</span><span style="color: #990000">=</span><span style="color: #993399">1</span> <span style="color: #009900">of</span><span style="color: #990000">=</span>main3<span style="color: #990000">.</span>inst</tt></pre></div></div>
|
||||
<div class="paragraph"><p>And that’s all there is to it.</p></div>
|
||||
<div class="paragraph"><p>Photonix was extracted in a similar way.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="footnotes"><hr /></div>
|
||||
<div id="footer">
|
||||
<div id="footer-text">
|
||||
Last updated
|
||||
2017-08-21 10:48:15 MST
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
95
docs/nucleus.txt
Normal file
95
docs/nucleus.txt
Normal file
@ -0,0 +1,95 @@
|
||||
Nucleus Demo
|
||||
============
|
||||
|
||||
Unfortunately, the Nucleus demo and Photonix tool do not use the Soundsmith
|
||||
format for their music. Instead they use a proprietary format. I decided to
|
||||
reverse engineer this format as well and make a player for them as well.
|
||||
|
||||
We, of course, start with loading the boot block:
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
./disasm nucleus.2mg 800 0 1 > boot.s
|
||||
----
|
||||
|
||||
Which lets us discover the loader is in blocks 7--b.
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
./disasm nucleus.2mg 9600 7 4 > loader.s
|
||||
----
|
||||
|
||||
The loader uses a table starting at `$9607`, where the first word is the
|
||||
starting block, the second word is the number of blocks, followed by a dword
|
||||
loading address. It repeats until the starting block has the high bit
|
||||
set. Nucleus does not use any compression for its data.
|
||||
|
||||
Using this, we discover the music player, which will let us discover where all
|
||||
the music data and wavebanks are located. (It will also be the thing I study
|
||||
the most in order to write a player).
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
./disasm nucleus.2mg 81000 333 4 > player.s
|
||||
----
|
||||
|
||||
The sound player is more akin to MIDI than a tracker. The songs are broken into
|
||||
channels, each channel controlling 4 oscillators. There aren't any patterns,
|
||||
there's just a long list of note frequencies and note lengths for each channel.
|
||||
This means each channel has its own play head, only advancing to the next note
|
||||
when the note length elapses. Each channel loops independently as well.
|
||||
|
||||
The first time the player initializes, it loads a wavebank from `$4/0000` into
|
||||
sound RAM. Then all future initializations load a wavebank from `$3/0000`.
|
||||
This means we have two wavebanks, one for the intro song, and the other for all
|
||||
the main songs in the demo. I use the table from the loader to determine which
|
||||
blocks are loaded for each memory location.
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
./decrunch nucleus.2mg 140 128 intro.wb raw
|
||||
./decrunch nucleus.2mg 12 128 main.wb raw
|
||||
----
|
||||
|
||||
The player uses a block of data located at `$8/0300` to determine all sorts of
|
||||
information about the songs and their channels. I'll be calling it `songdefs`.
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
./decrunch nucleus.2mg 268 2 songdefs raw
|
||||
----
|
||||
|
||||
This file is divided into chunks. Each chunk is 256 bytes long, and contains
|
||||
information about a song. So the first chunk contains information about the
|
||||
intro song. The second chunk contains information about the first main song,
|
||||
and so on. Using this we can determine there is 1 intro song, and 3 main songs.
|
||||
|
||||
Each chunk contains instrument data for each channel, as well as where in memory
|
||||
to find the note data for that song, as well has the playback speed.
|
||||
|
||||
The word at offset `$44` in the chunk determines where the note
|
||||
data for that specific song is located, inside bank 8. Using this we can
|
||||
extract the note data for all the songs.
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
./decrunch nucleus.2mg 284 8 intro.song raw
|
||||
./decrunch nucleus.2mg 270 14 main1.song raw
|
||||
./decrunch nucleus.2mg 292 7 main2.song raw
|
||||
./decrunch nucleus.2mg 299 2 main3.song raw
|
||||
----
|
||||
|
||||
I'll also divide up the songdefs file to provide easy access to each song's
|
||||
instrument data.
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
dd if=songdefs bs=256 skip=0 count=1 of=intro.inst
|
||||
dd if=songdefs bs=256 skip=1 count=1 of=main1.inst
|
||||
dd if=songdefs bs=256 skip=2 count=1 of=main2.inst
|
||||
dd if=songdefs bs=256 skip=3 count=1 of=main3.inst
|
||||
----
|
||||
|
||||
And that's all there is to it.
|
||||
|
||||
Photonix was extracted in a similar way.
|
1606
docs/player.html
Normal file
1606
docs/player.html
Normal file
File diff suppressed because it is too large
Load Diff
482
docs/player.txt
Normal file
482
docs/player.txt
Normal file
@ -0,0 +1,482 @@
|
||||
DOC info and Player Pseudocode
|
||||
==============================
|
||||
|
||||
== DOC
|
||||
|
||||
The DOC in the IIgs is a multi-channel digital oscillator. There are
|
||||
32 oscillators, operating in pairs. There is 64k of sound RAM, which holds
|
||||
the wavetable. The oscillators address into soundram and determine what
|
||||
sound data to send to the speaker.. the oscillators operate at a set
|
||||
frequency.
|
||||
|
||||
There are four registers for controlling the DOC.
|
||||
|
||||
`$c03c`::
|
||||
Sound Control. This uses various bits to control the other register modes.
|
||||
Bit 7::: DOC busy flag. 1 - DOC is busy
|
||||
Bit 6::: DOC or SoundRAM access. 0 - DOC
|
||||
Bit 5::: Address auto-increment. 1 - enabled
|
||||
Bit 4::: reserved
|
||||
Bits 3--0::: Master volume, 0 - low, 15 - high
|
||||
`$c03d`::
|
||||
Sound Data. This is used to read and write to and from the DOC and
|
||||
SoundRAM. If auto-increment is enabled, reading or writing to this register
|
||||
will auto-increment the address register. Note, when reading, the register
|
||||
lags by one cycle. You'll need to throw away the first read after modifying
|
||||
the address registers.
|
||||
`$c03e`::
|
||||
Address Low. This is the address into either the DOC or the SoundRAM.
|
||||
`$c03f`::
|
||||
Address High. This is the address into SoundRAM. When accessing the DOC,
|
||||
only the low byte of the address register is used.
|
||||
|
||||
=== DOC Addresses
|
||||
|
||||
When in DOC mode, you can modify various settings by setting the low address
|
||||
register to various addresses and writing and reading from the data register.
|
||||
The following are the various addresses used.
|
||||
|
||||
==== Oscillator Interrupt $E0
|
||||
|
||||
Contains which oscillator triggered an interrupt.
|
||||
|
||||
Bit 7::
|
||||
Interrupt occurred, 1 - yes
|
||||
Bits 5--1::
|
||||
Oscillator number that triggered the interrupt
|
||||
|
||||
==== Oscillator Enable $E1
|
||||
|
||||
The number of oscillators running. Multiply the number of desired oscillators
|
||||
by two, and set. Any number from 2 to 64 is valid. 2 is the default
|
||||
(1 oscillator).
|
||||
|
||||
==== A/D Converter $E2
|
||||
|
||||
This is the current value of the analog input.
|
||||
|
||||
==== Wavetable Size $C0--$DF
|
||||
|
||||
Control the size of the wavetable for each oscillator. $C0 controls
|
||||
oscillator 0, $DF controls oscillator 31.
|
||||
|
||||
Bits 5--3::
|
||||
Table size.
|
||||
0::: 256
|
||||
1::: 512
|
||||
2::: 1024
|
||||
3::: 2048
|
||||
4::: 4096
|
||||
5::: 8192
|
||||
6::: 16384
|
||||
7::: 32768
|
||||
Bits 2--0::
|
||||
Address resolution. See below for the wavetable address calculation.
|
||||
|
||||
==== Oscillator Control $A0--$BF
|
||||
|
||||
Control the oscillator behavior. $A0 is for oscillator 0, $BF is for
|
||||
oscillator 31.
|
||||
|
||||
Bits 7--4::
|
||||
Which hardware channel to use.
|
||||
Bit 3::
|
||||
Interrupt enable, 1 - interrupts enabled
|
||||
Bits 2--1::
|
||||
Oscillator mode
|
||||
0::: Free Run. Starts at beginning of wavetable and repeats same
|
||||
wavetable. Halts when halt bit is set, or 0 occurs in wavetable.
|
||||
1::: One Shot. Start at beginning of wavetable, step through once,
|
||||
stop at end of table.
|
||||
2::: Sync. When even-numbered oscillator starts, the oscillator above
|
||||
it will synchronize and begin simulatenously.
|
||||
3::: Swap. When even-numbered oscillator reaches end of wavetable,
|
||||
it resetsthe accumulator to 0, sets the halt bit, and clears the
|
||||
halt bit of the oscillator above it.
|
||||
Bit 0::
|
||||
Halt bit. 1 - Oscillator is halted.
|
||||
|
||||
==== Wavetable Pointers $80--$9F
|
||||
|
||||
The start page of each oscillator's wavetable. Each page is 256 bytes long.
|
||||
$80 is the start page of oscillator 0, $9F is the start page of oscillator 31.
|
||||
|
||||
==== Oscillator Data $60--$7F
|
||||
|
||||
The last byte read fro the wavetable for each oscillator. $60 is oscillator
|
||||
0, $7F is oscillator 31.
|
||||
|
||||
==== Volume $40--$5F
|
||||
|
||||
The oscillator's volume. The current wavetable data byte is multiplied
|
||||
by the 8-bit volume to obtain the final output level. $40 is the
|
||||
volume for oscillator 0, $5F is for oscillator 31.
|
||||
|
||||
==== Frequency High and Low $00--$3F
|
||||
|
||||
This is a 16-bit value for each oscillator. $00 is the low byte of the
|
||||
frequency for oscillator 0, $20 is the high byte for oscillator 0.
|
||||
|
||||
This determines the speed the wavetable is read from memory.
|
||||
|
||||
Output Frequency = F * SR / (2 ^ (17 + RES))
|
||||
|
||||
SR = 894.886KHz / (OSC + 2).
|
||||
|
||||
RES = Wavetable resolution
|
||||
|
||||
F = 16-bit frequency
|
||||
|
||||
OSC = number of enabled oscillators
|
||||
|
||||
=== Wavetable Address Calculation
|
||||
|
||||
Each oscillator has a 24-bit accumulator. Each time the oscillator
|
||||
updates, the 16-bit value from the oscillator's Frequency is added to
|
||||
the accumulator. The result is then passed to a multiplexer to determine
|
||||
the final 16-bit SoundRAM address. The Table Size, Wavetable Pointer, and
|
||||
Resolution all determine how the multiplexer works. Use the following
|
||||
table to determine how to calcualte the final address. The Pointer
|
||||
register determines the high bits of the address, the accumulatr determines
|
||||
the low bits.
|
||||
|
||||
[width="50%",options="header"]
|
||||
|==========================
|
||||
|Table Size|Resolution|Pointer Reg|Accumulator
|
||||
|256|7|P7--P0|A23--A16
|
||||
|256|6|P7--P0|A22--A15
|
||||
|256|5|P7--P0|A21--A14
|
||||
|256|...|...|...
|
||||
|256|0|P7--P0|A16--A9
|
||||
|512|7|P7--P1|A23--A15
|
||||
|512|6|P7--P1|A22--A14
|
||||
|512|...|...|...
|
||||
|512|0|P7--P1|A16--A8
|
||||
|1024|7|P7--P2|A23--A14
|
||||
|1024|6|P7--P2|A22--A13
|
||||
|1024|...|...|...
|
||||
|1024|0|P7--P2|A16--A7
|
||||
|2048|7|P7--P3|A23--A13
|
||||
|2048|6|P7--P3|A22--A12
|
||||
|2048|...|...|...
|
||||
|2048|0|P7--P3|A16--A6
|
||||
|4096|7|P7--P4|A23--A12
|
||||
|4096|6|P7--P4|A22--A11
|
||||
|4096|...|...|...
|
||||
|4096|0|P7--P4|A16--A5
|
||||
|8192|7|P7--P5|A23--A11
|
||||
|8192|6|P7--P5|A22--A10
|
||||
|8192|...|...|...
|
||||
|8192|0|P7--P5|A16--A4
|
||||
|16384|7|P7--P6|A23--A10
|
||||
|16384|6|P7--P6|A22--A9
|
||||
|16384|...|...|...
|
||||
|16384|0|P7--P6|A16--A3
|
||||
|32768|7|P7|A23--A9
|
||||
|32768|6|P7|A22--A8
|
||||
|32768|...|...|...
|
||||
|32768|0|P7|A16--A2
|
||||
|==========================
|
||||
|
||||
The 32 oscillators are serviced in sequence. With all oscillators
|
||||
enabled, the DOC takes 38 microseconds to service all 32. 1.2 microseconds
|
||||
per oscillator.
|
||||
|
||||
== Player
|
||||
|
||||
This is pseudocode for the soundsmith music player. The pseudocode style
|
||||
is basically C-style, with 68k-style word notation.
|
||||
|
||||
For example: `music[8].w` means read a little-endian word from the music
|
||||
array, starting at byte offset 8.
|
||||
|
||||
[source,c]
|
||||
----
|
||||
// parse the song headers and prep audio
|
||||
void initSong() {
|
||||
SNDCTL = CURVOL & 0xf; // set vol, enable DAC, disable autoinc
|
||||
// reset all oscillators to halt + freerun
|
||||
for (int osc = 0xa0; osc < 0xc0; osc++) {
|
||||
SNDADRL = osc;
|
||||
SNDDAT = 1; // halt
|
||||
}
|
||||
|
||||
// load wavebank into sound RAM
|
||||
SNDCTL = 0x60; // enable RAM + autoinc
|
||||
SNDADRL = 0;
|
||||
SNDADRH = 0; // point to beginning of sound RAM
|
||||
// do all 64k of sound RAM
|
||||
for (int addr = 0; addr < 0x10000; addr++) {
|
||||
SNDDAT = wavebank[addr + 2]; // skip num inst at start of wavebank
|
||||
}
|
||||
playing = false;
|
||||
|
||||
SNDINT = 0x945c; // = jsr $00/945c, this is soundInt()
|
||||
SNDINTH = 0x003c; // called whenever a channel stops
|
||||
|
||||
SNDCTL = 0; // DAC, vol = 0, disable autoinc
|
||||
|
||||
// use oscillator 0 as a timer
|
||||
SNDADRL = 0x0; // Osc 0 Frequency
|
||||
SNDDAT = 0xfa;
|
||||
SNDADRL = 0x20; // Osc 0 Frequency Hi
|
||||
SNDDAT = 0;
|
||||
SNDADRL = 0x40; // Osc 0 Volume
|
||||
SNDDAT = 0; // mute the timer
|
||||
SNDADRL = 0x80; // Osc 0 Wavetable ptr
|
||||
SNDDAT = 0;
|
||||
SNDADRL = 0xc0; // Osc 0 Wavetable size
|
||||
SNDDAT = 0; // 0 = 256 bytes, 0 res
|
||||
|
||||
SNDADRL = 0xe1; // Enable oscillators
|
||||
SNDDAT = 0x3c; // 30 oscillators
|
||||
SNDADRL = 0xa0; // Osc 0 control
|
||||
SNDDAT = 0x8; // free run mode + interrupts enabled
|
||||
|
||||
// music + header + blockSize = effects1 table (blockSize bytes long)
|
||||
effects1 = &music + 0x258 + music[6].w;
|
||||
// effects1 + blockSize = effects2 table (blockSize bytes long)
|
||||
effects2 = effects1 + music[6].w;
|
||||
// effects2 + blockSize = stereo table (16 words long)
|
||||
stereoTable = effects2 + music[6].w;
|
||||
|
||||
// load instrument headers
|
||||
int pos = 0;
|
||||
numInst = wavebank[0] & 0xff;
|
||||
for (inst = 0; inst < numInst; inst++) {
|
||||
for (int i = 0; i < 12; i++) {
|
||||
instdef[inst * 12 + i] = wavebank[0x10022 + pos++];
|
||||
}
|
||||
pos += 0x50;
|
||||
}
|
||||
// load compact table
|
||||
for (int y = 0; y < 0x20; y++) {
|
||||
compactTable[y] = wavebank[0x1005e + pos++];
|
||||
}
|
||||
}
|
||||
|
||||
// start playing the song
|
||||
void playSong() {
|
||||
timer = 0;
|
||||
songLen = music[0x1d6];
|
||||
curRow = 0;
|
||||
curPattern = 0;
|
||||
rowOffset = music[0x1d8] * 64 * 14;
|
||||
tempo = music[8].w;
|
||||
|
||||
int pos = 0;
|
||||
for (int i = 0; i < 0x1e; i += 2) {
|
||||
volumeTable[i].w = music[0x2c + pos].w;
|
||||
pos += 0x1e;
|
||||
}
|
||||
playing = true;
|
||||
}
|
||||
|
||||
// this is called whenever an oscillator halts with interrupts enabled
|
||||
void soundInt() {
|
||||
SNDCTL &= 0x9f; // doc, no auto inc
|
||||
SNDADRL = 0xe0; // oscillator interrupt
|
||||
SNDDAT &= 0x7f; // clear interrupt
|
||||
uint8_t osc = (SNDDAT & 0x3e) >> 1; // get fired oscillator
|
||||
if (osc != 0) { // wasn't timer
|
||||
SNDADRL = 0xa0 + osc; // osc control
|
||||
if (SNDDAT & 8) { // were interrupts enabled?
|
||||
SNDDAT &= 0xfe; // clear halt bit.. retrig
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!playing)
|
||||
return;
|
||||
timer++;
|
||||
if (timer == tempo) {
|
||||
timer = 0;
|
||||
for (oscillator = 0; oscillator < 0xe; oscillator++) {
|
||||
semitone = music[0x258 + rowOffset];
|
||||
if (semitone == 0 || semitone >= 0x80) {
|
||||
rowOffset++;
|
||||
if (semitone == 0x80) {
|
||||
SNDCTL &= 0x9f; // DAC mode
|
||||
oscaddr = (oscillator + 1) * 2;
|
||||
SNDADRL = 0xa0 + oscaddr; // osc control
|
||||
SNDDAT = 1; // halt
|
||||
SNDADRL = 0xa0 + oscaddr + 1; // osc control
|
||||
SNDDAT = 1; // halt pair
|
||||
} else if (semitone == 0x81) {
|
||||
curRow = 0x3f;
|
||||
}
|
||||
} else {
|
||||
uint8_t fx = effects1[rowOffset];
|
||||
uint8_t inst = fx & 0xf0;
|
||||
if (!inst)
|
||||
inst = prevInst[oscillator];
|
||||
prevInst[oscillator] = inst;
|
||||
volumeInt = volumeTable[((inst >> 4) - 1) * 2].w / 2;
|
||||
fx &= 0xf;
|
||||
if (fx == 0) {
|
||||
arpeggio[oscillator] = effects2[rowOffset];
|
||||
arpTone[oscillator] = semitone;
|
||||
} else {
|
||||
arpeggio[oscillator] = 0;
|
||||
if (fx == 3) {
|
||||
volumeInt = effects2[rowOffset] / 2;
|
||||
} else if (fx == 6) {
|
||||
volumeInt -= effects2[rowOffset] / 2;
|
||||
if (volumeInt < 0)
|
||||
volumeInt = 0;
|
||||
} else if (fx == 5) {
|
||||
volumeInt += effects2[rowOffset] / 2;
|
||||
if (volumeInt >= 0x80)
|
||||
volumeInt = 0x7f;
|
||||
} else if (fx == 0xf) {
|
||||
tempo = effects2[rowOffset];
|
||||
}
|
||||
if ((fx == 3 || fx == 5 || fx == 6) && semitone == 0) {
|
||||
while (SNDCTL & 0x80); // wait for DOC
|
||||
SNDCTL = (SNDCTL | 0x20) & 0xbf; // DOC + autoinc
|
||||
SNDADRL = 0x40 + (oscillator + 1) * 2; // osc volume
|
||||
SNDDAT = volumeInt;
|
||||
SNDDAT = volumeInt; // pair
|
||||
}
|
||||
}
|
||||
|
||||
if (semitone) {
|
||||
oscaddr = (oscillator + 1) * 2;
|
||||
SNDCTL &= 0x9f; // DOC mode
|
||||
SNDADRL = 0xa0 + oscaddr; // osc ctl
|
||||
SNDDAT = (SNDDAT & 0xf7) | 1; // halt, no interrupt
|
||||
SNDADRL = 0xa0 + oscaddr + 1; // osc ctl pair
|
||||
SNDDAT = (SNDDAT & 0xf7) | 1; // halt, no interrupt pair
|
||||
inst = (prevInst[oscillator] >> 4) - 1;
|
||||
if (inst < numInst) {
|
||||
int x = inst * 12;
|
||||
while (instruments[x].b < semitone) {
|
||||
x += 6;
|
||||
}
|
||||
oscAptr = instruments[x + 1].b;
|
||||
oscAsiz = instruments[x + 2].b;
|
||||
oscActl = instruments[x + 3].b;
|
||||
if (stereo) {
|
||||
oscActl &= 0xf;
|
||||
if (stereoTable[oscillator * 2])
|
||||
oscActl |= 0x10;
|
||||
}
|
||||
while (instruments[x].b != 0x7f) {
|
||||
x += 6;
|
||||
}
|
||||
x += 6; // skip last instdef
|
||||
while (instruments[x] < semitone) {
|
||||
x += 6;
|
||||
}
|
||||
oscBptr = instruments[x + 1].b;
|
||||
oscBsiz = instruments[x + 2].b;
|
||||
oscBctl = instruments[x + 3].b;
|
||||
if (stereo) {
|
||||
oscBctl &= 0xf;
|
||||
if (stereoTable[oscillator * 2])
|
||||
oscBctl |= 0x10;
|
||||
}
|
||||
freq = freqTable[semitone * 2].w >> compactTable[inst * 2].w;
|
||||
while (SNDCTL & 0x80); // wait for DOC
|
||||
SNDCTL = (SNDCTL | 0x20) & 0xbf; // DOC + autoinc
|
||||
SNDADRL = oscaddr; // osc freq lo
|
||||
SNDDAT = freq;
|
||||
SNDDAT = freq; // pair
|
||||
SNDADRL = 0x20 + oscaddr; // osc freq hi
|
||||
SNDDAT = freq >> 8;
|
||||
SNDDAT = freq >> 8; // pair
|
||||
SNDADRL = 0x40 + oscaddr; // osc volume
|
||||
SNDDAT = volumeConversion[volumeInt];
|
||||
SNDDAT = volumeConversion[volumeInt]; // pair
|
||||
SNDADRL = 0x80 + oscaddr; // osc wavetable ptr
|
||||
SNDDAT = oscAptr;
|
||||
SNDDAT = oscBptr; // pair
|
||||
SNDADRL = 0xc0 + oscaddr; // osc wavetable size
|
||||
SNDDAT = oscAsiz;
|
||||
SNDDAT = oscBsiz; // pair
|
||||
SNDADRL = 0xa0 + oscaddr; // osc ctl
|
||||
SNDDAT = oscActl;
|
||||
SNDDAT = oscBctl; // pair
|
||||
}
|
||||
}
|
||||
rowOffset++;
|
||||
}
|
||||
}
|
||||
curRow++;
|
||||
if (curRow < 0x40)
|
||||
return;
|
||||
// advance pattern
|
||||
curRow = 0;
|
||||
curPattern++;
|
||||
if (curPattern < songLen) {
|
||||
rowOffset = music[0x1d8 + curPattern] * 64 * 14;
|
||||
} else { // stopped
|
||||
playing = false;
|
||||
}
|
||||
return;
|
||||
} else { // between notes.. apply arpeggios
|
||||
for (oscillator = 0; oscillator < 0xe; oscillator++) {
|
||||
if (arpeggio[oscillator]) {
|
||||
switch (timer % 6) {
|
||||
case 1: case 4:
|
||||
arpTone[oscillator] += arpeggio[oscillator] >> 4;
|
||||
break;
|
||||
case 2: case 5:
|
||||
arpTone[oscillator] += arpeggio[oscillator] & 0xf;
|
||||
break;
|
||||
case 0: case 3:
|
||||
arpTone[oscillator] -= arpeggio[oscillator] >> 4;
|
||||
arpTone[oscillator] -= arpeggio[oscillator] & 0xf;
|
||||
break;
|
||||
}
|
||||
freq = freqTable[arpTone[oscillator] * 2].w >> compactTable[oscillator *
|
||||
2].w;
|
||||
oscaddr = (oscillator + 1) * 2;
|
||||
while (SNDCTL & 0x80); // wait for DOC
|
||||
SNDCTL = (SNDCTL | 0x20) & 0xbf; // DOC + autoinc
|
||||
SNDADRL = oscaddr; // freq lo
|
||||
SNDDAT = freq;
|
||||
SNDDAT = freq; // pair
|
||||
SNDADRL = 0x20 + oscaddr; // freq hi
|
||||
SNDDAT = freq >> 8;
|
||||
SNDDAT = freq >> 8; // pair
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t volumeConversion[] = {
|
||||
0x00, 0x02, 0x04, 0x05, 0x06, 0x07, 0x09, 0x0a, // 0
|
||||
0x0c, 0x0d, 0x0f, 0x10, 0x12, 0x13, 0x15, 0x16, // 8
|
||||
0x18, 0x19, 0x1b, 0x1c, 0x1e, 0x1f, 0x21, 0x22, // 10
|
||||
0x24, 0x25, 0x27, 0x28, 0x2a, 0x2b, 0x2d, 0x2e, // 18
|
||||
0x30, 0x31, 0x33, 0x34, 0x36, 0x37, 0x39, 0x3a, // 20
|
||||
0x3c, 0x3d, 0x3f, 0x40, 0x42, 0x43, 0x45, 0x46, // 28
|
||||
0x48, 0x49, 0x4b, 0x4c, 0x4e, 0x4f, 0x51, 0x52, // 30
|
||||
0x54, 0x55, 0x57, 0x58, 0x5a, 0x5b, 0x5d, 0x5e, // 38
|
||||
0x60, 0x61, 0x63, 0x64, 0x66, 0x67, 0x69, 0x6a, // 40
|
||||
0x6c, 0x6d, 0x6f, 0x70, 0x72, 0x73, 0x75, 0x76, // 48
|
||||
0x78, 0x79, 0x7b, 0x7c, 0x7e, 0x7f, 0x81, 0x82, // 50
|
||||
0x84, 0x85, 0x87, 0x88, 0x8a, 0x8b, 0x8d, 0x8e, // 58
|
||||
0x90, 0x91, 0x93, 0x94, 0x96, 0x97, 0x99, 0x9a, // 60
|
||||
0x9c, 0x9d, 0x9f, 0xa0, 0xa2, 0xa3, 0xa5, 0xa6, // 68
|
||||
0xa8, 0xa9, 0xab, 0xac, 0xae, 0xaf, 0xb1, 0xb2, // 70
|
||||
0xb4, 0xb5, 0xb7, 0xb8, 0xba, 0xbb, 0xbe, 0xc0 // 78
|
||||
};
|
||||
uint16_t freqTable[] = {
|
||||
0x0000, 0x0016, 0x0017, 0x0018, 0x001a, 0x001b, 0x001d, 0x001e,
|
||||
0x0020, 0x0022, 0x0024, 0x0026, 0x0029, 0x002b, 0x002e, 0x0031,
|
||||
0x0033, 0x0036, 0x003a, 0x003d, 0x0041, 0x0045, 0x0049, 0x004d,
|
||||
0x0052, 0x0056, 0x005c, 0x0061, 0x0067, 0x006d, 0x0073, 0x007a,
|
||||
0x0081, 0x0089, 0x0091, 0x009a, 0x00a3, 0x00ad, 0x00b7, 0x00c2,
|
||||
0x00ce, 0x00d9, 0x00e6, 0x00f4, 0x0102, 0x0112, 0x0122, 0x0133,
|
||||
0x0146, 0x015a, 0x016f, 0x0184, 0x019b, 0x01b4, 0x01ce, 0x01e9,
|
||||
0x0206, 0x0225, 0x0246, 0x0269, 0x028d, 0x02b4, 0x02dd, 0x0309,
|
||||
0x0337, 0x0368, 0x039c, 0x03d3, 0x040d, 0x044a, 0x048c, 0x04d1,
|
||||
0x051a, 0x0568, 0x05ba, 0x0611, 0x066e, 0x06d0, 0x0737, 0x07a5,
|
||||
0x081a, 0x0895, 0x0918, 0x09a2, 0x0a35, 0x0ad0, 0x0b75, 0x0c23,
|
||||
0x0cdc, 0x0d9f, 0x0e6f, 0x0f4b, 0x1033, 0x112a, 0x122f, 0x1344,
|
||||
0x1469, 0x15a0, 0x16e9, 0x1846, 0x19b7, 0x1b3f, 0x1cde, 0x1e95,
|
||||
0x2066, 0x2254, 0x245e, 0x2688
|
||||
};
|
||||
----
|
976
docs/xmas.html
Normal file
976
docs/xmas.html
Normal file
@ -0,0 +1,976 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
|
||||
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="application/xhtml+xml; charset=UTF-8" />
|
||||
<meta name="generator" content="AsciiDoc 8.6.9" />
|
||||
<title>Extracting music from the Xmas demo</title>
|
||||
<style type="text/css">
|
||||
/* Shared CSS for AsciiDoc xhtml11 and html5 backends */
|
||||
|
||||
/* Default font. */
|
||||
body {
|
||||
font-family: Georgia,serif;
|
||||
}
|
||||
|
||||
/* Title font. */
|
||||
h1, h2, h3, h4, h5, h6,
|
||||
div.title, caption.title,
|
||||
thead, p.table.header,
|
||||
#toctitle,
|
||||
#author, #revnumber, #revdate, #revremark,
|
||||
#footer {
|
||||
font-family: Arial,Helvetica,sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 1em 5% 1em 5%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:visited {
|
||||
color: fuchsia;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: italic;
|
||||
color: navy;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
color: #083194;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: #527bbd;
|
||||
margin-top: 1.2em;
|
||||
margin-bottom: 0.5em;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
border-bottom: 2px solid silver;
|
||||
}
|
||||
h2 {
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
h3 {
|
||||
float: left;
|
||||
}
|
||||
h3 + * {
|
||||
clear: left;
|
||||
}
|
||||
h5 {
|
||||
font-size: 1.0em;
|
||||
}
|
||||
|
||||
div.sectionbody {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 1px solid silver;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
ul, ol, li > p {
|
||||
margin-top: 0;
|
||||
}
|
||||
ul > li { color: #aaa; }
|
||||
ul > li > * { color: black; }
|
||||
|
||||
.monospaced, code, pre {
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
font-size: inherit;
|
||||
color: navy;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
#author {
|
||||
color: #527bbd;
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
#email {
|
||||
}
|
||||
#revnumber, #revdate, #revremark {
|
||||
}
|
||||
|
||||
#footer {
|
||||
font-size: small;
|
||||
border-top: 2px solid silver;
|
||||
padding-top: 0.5em;
|
||||
margin-top: 4.0em;
|
||||
}
|
||||
#footer-text {
|
||||
float: left;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
#footer-badges {
|
||||
float: right;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
|
||||
#preamble {
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
div.imageblock, div.exampleblock, div.verseblock,
|
||||
div.quoteblock, div.literalblock, div.listingblock, div.sidebarblock,
|
||||
div.admonitionblock {
|
||||
margin-top: 1.0em;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
div.admonitionblock {
|
||||
margin-top: 2.0em;
|
||||
margin-bottom: 2.0em;
|
||||
margin-right: 10%;
|
||||
color: #606060;
|
||||
}
|
||||
|
||||
div.content { /* Block element content. */
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Block element titles. */
|
||||
div.title, caption.title {
|
||||
color: #527bbd;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
margin-top: 1.0em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
div.title + * {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
td div.title:first-child {
|
||||
margin-top: 0.0em;
|
||||
}
|
||||
div.content div.title:first-child {
|
||||
margin-top: 0.0em;
|
||||
}
|
||||
div.content + div.title {
|
||||
margin-top: 0.0em;
|
||||
}
|
||||
|
||||
div.sidebarblock > div.content {
|
||||
background: #ffffee;
|
||||
border: 1px solid #dddddd;
|
||||
border-left: 4px solid #f0f0f0;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
div.listingblock > div.content {
|
||||
border: 1px solid #dddddd;
|
||||
border-left: 5px solid #f0f0f0;
|
||||
background: #f8f8f8;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
div.quoteblock, div.verseblock {
|
||||
padding-left: 1.0em;
|
||||
margin-left: 1.0em;
|
||||
margin-right: 10%;
|
||||
border-left: 5px solid #f0f0f0;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
div.quoteblock > div.attribution {
|
||||
padding-top: 0.5em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
div.verseblock > pre.content {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
div.verseblock > div.attribution {
|
||||
padding-top: 0.75em;
|
||||
text-align: left;
|
||||
}
|
||||
/* DEPRECATED: Pre version 8.2.7 verse style literal block. */
|
||||
div.verseblock + div.attribution {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
div.admonitionblock .icon {
|
||||
vertical-align: top;
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
text-decoration: underline;
|
||||
color: #527bbd;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
div.admonitionblock td.content {
|
||||
padding-left: 0.5em;
|
||||
border-left: 3px solid #dddddd;
|
||||
}
|
||||
|
||||
div.exampleblock > div.content {
|
||||
border-left: 3px solid #dddddd;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
div.imageblock div.content { padding-left: 0; }
|
||||
span.image img { border-style: none; vertical-align: text-bottom; }
|
||||
a.image:visited { color: white; }
|
||||
|
||||
dl {
|
||||
margin-top: 0.8em;
|
||||
margin-bottom: 0.8em;
|
||||
}
|
||||
dt {
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0;
|
||||
font-style: normal;
|
||||
color: navy;
|
||||
}
|
||||
dd > *:first-child {
|
||||
margin-top: 0.1em;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
list-style-position: outside;
|
||||
}
|
||||
ol.arabic {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
ol.loweralpha {
|
||||
list-style-type: lower-alpha;
|
||||
}
|
||||
ol.upperalpha {
|
||||
list-style-type: upper-alpha;
|
||||
}
|
||||
ol.lowerroman {
|
||||
list-style-type: lower-roman;
|
||||
}
|
||||
ol.upperroman {
|
||||
list-style-type: upper-roman;
|
||||
}
|
||||
|
||||
div.compact ul, div.compact ol,
|
||||
div.compact p, div.compact p,
|
||||
div.compact div, div.compact div {
|
||||
margin-top: 0.1em;
|
||||
margin-bottom: 0.1em;
|
||||
}
|
||||
|
||||
tfoot {
|
||||
font-weight: bold;
|
||||
}
|
||||
td > div.verse {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
div.hdlist {
|
||||
margin-top: 0.8em;
|
||||
margin-bottom: 0.8em;
|
||||
}
|
||||
div.hdlist tr {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
dt.hdlist1.strong, td.hdlist1.strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
td.hdlist1 {
|
||||
vertical-align: top;
|
||||
font-style: normal;
|
||||
padding-right: 0.8em;
|
||||
color: navy;
|
||||
}
|
||||
td.hdlist2 {
|
||||
vertical-align: top;
|
||||
}
|
||||
div.hdlist.compact tr {
|
||||
margin: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.comment {
|
||||
background: yellow;
|
||||
}
|
||||
|
||||
.footnote, .footnoteref {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
span.footnote, span.footnoteref {
|
||||
vertical-align: super;
|
||||
}
|
||||
|
||||
#footnotes {
|
||||
margin: 20px 0 20px 0;
|
||||
padding: 7px 0 0 0;
|
||||
}
|
||||
|
||||
#footnotes div.footnote {
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
|
||||
#footnotes hr {
|
||||
border: none;
|
||||
border-top: 1px solid silver;
|
||||
height: 1px;
|
||||
text-align: left;
|
||||
margin-left: 0;
|
||||
width: 20%;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
div.colist td {
|
||||
padding-right: 0.5em;
|
||||
padding-bottom: 0.3em;
|
||||
vertical-align: top;
|
||||
}
|
||||
div.colist td img {
|
||||
margin-top: 0.3em;
|
||||
}
|
||||
|
||||
@media print {
|
||||
#footer-badges { display: none; }
|
||||
}
|
||||
|
||||
#toc {
|
||||
margin-bottom: 2.5em;
|
||||
}
|
||||
|
||||
#toctitle {
|
||||
color: #527bbd;
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
margin-top: 1.0em;
|
||||
margin-bottom: 0.1em;
|
||||
}
|
||||
|
||||
div.toclevel0, div.toclevel1, div.toclevel2, div.toclevel3, div.toclevel4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
div.toclevel2 {
|
||||
margin-left: 2em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
div.toclevel3 {
|
||||
margin-left: 4em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
div.toclevel4 {
|
||||
margin-left: 6em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
span.aqua { color: aqua; }
|
||||
span.black { color: black; }
|
||||
span.blue { color: blue; }
|
||||
span.fuchsia { color: fuchsia; }
|
||||
span.gray { color: gray; }
|
||||
span.green { color: green; }
|
||||
span.lime { color: lime; }
|
||||
span.maroon { color: maroon; }
|
||||
span.navy { color: navy; }
|
||||
span.olive { color: olive; }
|
||||
span.purple { color: purple; }
|
||||
span.red { color: red; }
|
||||
span.silver { color: silver; }
|
||||
span.teal { color: teal; }
|
||||
span.white { color: white; }
|
||||
span.yellow { color: yellow; }
|
||||
|
||||
span.aqua-background { background: aqua; }
|
||||
span.black-background { background: black; }
|
||||
span.blue-background { background: blue; }
|
||||
span.fuchsia-background { background: fuchsia; }
|
||||
span.gray-background { background: gray; }
|
||||
span.green-background { background: green; }
|
||||
span.lime-background { background: lime; }
|
||||
span.maroon-background { background: maroon; }
|
||||
span.navy-background { background: navy; }
|
||||
span.olive-background { background: olive; }
|
||||
span.purple-background { background: purple; }
|
||||
span.red-background { background: red; }
|
||||
span.silver-background { background: silver; }
|
||||
span.teal-background { background: teal; }
|
||||
span.white-background { background: white; }
|
||||
span.yellow-background { background: yellow; }
|
||||
|
||||
span.big { font-size: 2em; }
|
||||
span.small { font-size: 0.6em; }
|
||||
|
||||
span.underline { text-decoration: underline; }
|
||||
span.overline { text-decoration: overline; }
|
||||
span.line-through { text-decoration: line-through; }
|
||||
|
||||
div.unbreakable { page-break-inside: avoid; }
|
||||
|
||||
|
||||
/*
|
||||
* xhtml11 specific
|
||||
*
|
||||
* */
|
||||
|
||||
div.tableblock {
|
||||
margin-top: 1.0em;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
div.tableblock > table {
|
||||
border: 3px solid #527bbd;
|
||||
}
|
||||
thead, p.table.header {
|
||||
font-weight: bold;
|
||||
color: #527bbd;
|
||||
}
|
||||
p.table {
|
||||
margin-top: 0;
|
||||
}
|
||||
/* Because the table frame attribute is overriden by CSS in most browsers. */
|
||||
div.tableblock > table[frame="void"] {
|
||||
border-style: none;
|
||||
}
|
||||
div.tableblock > table[frame="hsides"] {
|
||||
border-left-style: none;
|
||||
border-right-style: none;
|
||||
}
|
||||
div.tableblock > table[frame="vsides"] {
|
||||
border-top-style: none;
|
||||
border-bottom-style: none;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* html5 specific
|
||||
*
|
||||
* */
|
||||
|
||||
table.tableblock {
|
||||
margin-top: 1.0em;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
thead, p.tableblock.header {
|
||||
font-weight: bold;
|
||||
color: #527bbd;
|
||||
}
|
||||
p.tableblock {
|
||||
margin-top: 0;
|
||||
}
|
||||
table.tableblock {
|
||||
border-width: 3px;
|
||||
border-spacing: 0px;
|
||||
border-style: solid;
|
||||
border-color: #527bbd;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
th.tableblock, td.tableblock {
|
||||
border-width: 1px;
|
||||
padding: 4px;
|
||||
border-style: solid;
|
||||
border-color: #527bbd;
|
||||
}
|
||||
|
||||
table.tableblock.frame-topbot {
|
||||
border-left-style: hidden;
|
||||
border-right-style: hidden;
|
||||
}
|
||||
table.tableblock.frame-sides {
|
||||
border-top-style: hidden;
|
||||
border-bottom-style: hidden;
|
||||
}
|
||||
table.tableblock.frame-none {
|
||||
border-style: hidden;
|
||||
}
|
||||
|
||||
th.tableblock.halign-left, td.tableblock.halign-left {
|
||||
text-align: left;
|
||||
}
|
||||
th.tableblock.halign-center, td.tableblock.halign-center {
|
||||
text-align: center;
|
||||
}
|
||||
th.tableblock.halign-right, td.tableblock.halign-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
th.tableblock.valign-top, td.tableblock.valign-top {
|
||||
vertical-align: top;
|
||||
}
|
||||
th.tableblock.valign-middle, td.tableblock.valign-middle {
|
||||
vertical-align: middle;
|
||||
}
|
||||
th.tableblock.valign-bottom, td.tableblock.valign-bottom {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* manpage specific
|
||||
*
|
||||
* */
|
||||
|
||||
body.manpage h1 {
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
border-top: 2px solid silver;
|
||||
border-bottom: 2px solid silver;
|
||||
}
|
||||
body.manpage h2 {
|
||||
border-style: none;
|
||||
}
|
||||
body.manpage div.sectionbody {
|
||||
margin-left: 3em;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body.manpage div#toc { display: none; }
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
/*<![CDATA[*/
|
||||
var asciidoc = { // Namespace.
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// Table Of Contents generator
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
/* Author: Mihai Bazon, September 2002
|
||||
* http://students.infoiasi.ro/~mishoo
|
||||
*
|
||||
* Table Of Content generator
|
||||
* Version: 0.4
|
||||
*
|
||||
* Feel free to use this script under the terms of the GNU General Public
|
||||
* License, as long as you do not remove or alter this notice.
|
||||
*/
|
||||
|
||||
/* modified by Troy D. Hanson, September 2006. License: GPL */
|
||||
/* modified by Stuart Rackham, 2006, 2009. License: GPL */
|
||||
|
||||
// toclevels = 1..4.
|
||||
toc: function (toclevels) {
|
||||
|
||||
function getText(el) {
|
||||
var text = "";
|
||||
for (var i = el.firstChild; i != null; i = i.nextSibling) {
|
||||
if (i.nodeType == 3 /* Node.TEXT_NODE */) // IE doesn't speak constants.
|
||||
text += i.data;
|
||||
else if (i.firstChild != null)
|
||||
text += getText(i);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function TocEntry(el, text, toclevel) {
|
||||
this.element = el;
|
||||
this.text = text;
|
||||
this.toclevel = toclevel;
|
||||
}
|
||||
|
||||
function tocEntries(el, toclevels) {
|
||||
var result = new Array;
|
||||
var re = new RegExp('[hH]([1-'+(toclevels+1)+'])');
|
||||
// Function that scans the DOM tree for header elements (the DOM2
|
||||
// nodeIterator API would be a better technique but not supported by all
|
||||
// browsers).
|
||||
var iterate = function (el) {
|
||||
for (var i = el.firstChild; i != null; i = i.nextSibling) {
|
||||
if (i.nodeType == 1 /* Node.ELEMENT_NODE */) {
|
||||
var mo = re.exec(i.tagName);
|
||||
if (mo && (i.getAttribute("class") || i.getAttribute("className")) != "float") {
|
||||
result[result.length] = new TocEntry(i, getText(i), mo[1]-1);
|
||||
}
|
||||
iterate(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
iterate(el);
|
||||
return result;
|
||||
}
|
||||
|
||||
var toc = document.getElementById("toc");
|
||||
if (!toc) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete existing TOC entries in case we're reloading the TOC.
|
||||
var tocEntriesToRemove = [];
|
||||
var i;
|
||||
for (i = 0; i < toc.childNodes.length; i++) {
|
||||
var entry = toc.childNodes[i];
|
||||
if (entry.nodeName.toLowerCase() == 'div'
|
||||
&& entry.getAttribute("class")
|
||||
&& entry.getAttribute("class").match(/^toclevel/))
|
||||
tocEntriesToRemove.push(entry);
|
||||
}
|
||||
for (i = 0; i < tocEntriesToRemove.length; i++) {
|
||||
toc.removeChild(tocEntriesToRemove[i]);
|
||||
}
|
||||
|
||||
// Rebuild TOC entries.
|
||||
var entries = tocEntries(document.getElementById("content"), toclevels);
|
||||
for (var i = 0; i < entries.length; ++i) {
|
||||
var entry = entries[i];
|
||||
if (entry.element.id == "")
|
||||
entry.element.id = "_toc_" + i;
|
||||
var a = document.createElement("a");
|
||||
a.href = "#" + entry.element.id;
|
||||
a.appendChild(document.createTextNode(entry.text));
|
||||
var div = document.createElement("div");
|
||||
div.appendChild(a);
|
||||
div.className = "toclevel" + entry.toclevel;
|
||||
toc.appendChild(div);
|
||||
}
|
||||
if (entries.length == 0)
|
||||
toc.parentNode.removeChild(toc);
|
||||
},
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// Footnotes generator
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
/* Based on footnote generation code from:
|
||||
* http://www.brandspankingnew.net/archive/2005/07/format_footnote.html
|
||||
*/
|
||||
|
||||
footnotes: function () {
|
||||
// Delete existing footnote entries in case we're reloading the footnodes.
|
||||
var i;
|
||||
var noteholder = document.getElementById("footnotes");
|
||||
if (!noteholder) {
|
||||
return;
|
||||
}
|
||||
var entriesToRemove = [];
|
||||
for (i = 0; i < noteholder.childNodes.length; i++) {
|
||||
var entry = noteholder.childNodes[i];
|
||||
if (entry.nodeName.toLowerCase() == 'div' && entry.getAttribute("class") == "footnote")
|
||||
entriesToRemove.push(entry);
|
||||
}
|
||||
for (i = 0; i < entriesToRemove.length; i++) {
|
||||
noteholder.removeChild(entriesToRemove[i]);
|
||||
}
|
||||
|
||||
// Rebuild footnote entries.
|
||||
var cont = document.getElementById("content");
|
||||
var spans = cont.getElementsByTagName("span");
|
||||
var refs = {};
|
||||
var n = 0;
|
||||
for (i=0; i<spans.length; i++) {
|
||||
if (spans[i].className == "footnote") {
|
||||
n++;
|
||||
var note = spans[i].getAttribute("data-note");
|
||||
if (!note) {
|
||||
// Use [\s\S] in place of . so multi-line matches work.
|
||||
// Because JavaScript has no s (dotall) regex flag.
|
||||
note = spans[i].innerHTML.match(/\s*\[([\s\S]*)]\s*/)[1];
|
||||
spans[i].innerHTML =
|
||||
"[<a id='_footnoteref_" + n + "' href='#_footnote_" + n +
|
||||
"' title='View footnote' class='footnote'>" + n + "</a>]";
|
||||
spans[i].setAttribute("data-note", note);
|
||||
}
|
||||
noteholder.innerHTML +=
|
||||
"<div class='footnote' id='_footnote_" + n + "'>" +
|
||||
"<a href='#_footnoteref_" + n + "' title='Return to text'>" +
|
||||
n + "</a>. " + note + "</div>";
|
||||
var id =spans[i].getAttribute("id");
|
||||
if (id != null) refs["#"+id] = n;
|
||||
}
|
||||
}
|
||||
if (n == 0)
|
||||
noteholder.parentNode.removeChild(noteholder);
|
||||
else {
|
||||
// Process footnoterefs.
|
||||
for (i=0; i<spans.length; i++) {
|
||||
if (spans[i].className == "footnoteref") {
|
||||
var href = spans[i].getElementsByTagName("a")[0].getAttribute("href");
|
||||
href = href.match(/#.*/)[0]; // Because IE return full URL.
|
||||
n = refs[href];
|
||||
spans[i].innerHTML =
|
||||
"[<a href='#_footnote_" + n +
|
||||
"' title='View footnote' class='footnote'>" + n + "</a>]";
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
install: function(toclevels) {
|
||||
var timerId;
|
||||
|
||||
function reinstall() {
|
||||
asciidoc.footnotes();
|
||||
if (toclevels) {
|
||||
asciidoc.toc(toclevels);
|
||||
}
|
||||
}
|
||||
|
||||
function reinstallAndRemoveTimer() {
|
||||
clearInterval(timerId);
|
||||
reinstall();
|
||||
}
|
||||
|
||||
timerId = setInterval(reinstall, 500);
|
||||
if (document.addEventListener)
|
||||
document.addEventListener("DOMContentLoaded", reinstallAndRemoveTimer, false);
|
||||
else
|
||||
window.onload = reinstallAndRemoveTimer;
|
||||
}
|
||||
|
||||
}
|
||||
asciidoc.install();
|
||||
/*]]>*/
|
||||
</script>
|
||||
</head>
|
||||
<body class="article">
|
||||
<div id="header">
|
||||
<h1>Extracting music from the Xmas demo</h1>
|
||||
</div>
|
||||
<div id="content">
|
||||
<div id="preamble">
|
||||
<div class="sectionbody">
|
||||
<div class="paragraph"><p>This is very similar to the documentation on extracting music from Modulae.
|
||||
The steps are a little more involved since the Xmas demo is a multi-part
|
||||
demo, where the code and music for each part is loaded from disk separately.</p></div>
|
||||
<div class="paragraph"><p>As with Modulae, we’ll start with extracting the initial boot loader.</p></div>
|
||||
<div class="listingblock">
|
||||
<div class="content"><!-- Generator: GNU source-highlight 3.1.8
|
||||
by Lorenzo Bettini
|
||||
http://www.lorenzobettini.it
|
||||
http://www.gnu.org/software/src-highlite -->
|
||||
<pre><tt><span style="color: #990000">.</span>/disasm xmasdemo<span style="color: #990000">.</span>2mg <span style="color: #993399">800</span> <span style="color: #993399">0</span> <span style="color: #993399">1</span> <span style="color: #990000">></span> boot<span style="color: #990000">.</span>s</tt></pre></div></div>
|
||||
<div class="paragraph"><p>This time, the loading starts at <code>$08e3</code>. It loads blocks 7—17 into
|
||||
RAM starting at <code>$9000</code> and then calls the decrunch routine at <code>$0a00</code>.</p></div>
|
||||
<div class="paragraph"><p>We’ll do the same to extract the main loader.</p></div>
|
||||
<div class="listingblock">
|
||||
<div class="content"><!-- Generator: GNU source-highlight 3.1.8
|
||||
by Lorenzo Bettini
|
||||
http://www.lorenzobettini.it
|
||||
http://www.gnu.org/software/src-highlite -->
|
||||
<pre><tt><span style="color: #990000">.</span>/decrunch xmasdemo<span style="color: #990000">.</span>2mg <span style="color: #993399">7</span> <span style="color: #993399">11</span> loader
|
||||
<span style="color: #990000">.</span>/disasm loader <span style="color: #993399">9000</span> <span style="color: #990000">></span> loader<span style="color: #990000">.</span>s</tt></pre></div></div>
|
||||
<div class="paragraph"><p>Again, we track down the loading routine. This time, it’s a function
|
||||
located at <code>$9c3f</code>. It gets called multiple times, to load the different
|
||||
parts of the demo. The <code>A</code> register holds the address of the table to use
|
||||
when loading.</p></div>
|
||||
<div class="paragraph"><p>The first time the loader is called, it uses the table at <code>$9ac3</code>. This
|
||||
table only contains the loading screen graphics. So we’ll ignore it.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sect1">
|
||||
<h2 id="_loading_8230_music">Loading… Music</h2>
|
||||
<div class="sectionbody">
|
||||
<div class="paragraph"><p>The second time the loader is called, it uses the table at <code>$9ad5</code>.
|
||||
We’ll extract the table offsets as we did in the Modulae example. Using
|
||||
the relative address of the table from the start of the loader file.</p></div>
|
||||
<div class="listingblock">
|
||||
<div class="content"><!-- Generator: GNU source-highlight 3.1.8
|
||||
by Lorenzo Bettini
|
||||
http://www.lorenzobettini.it
|
||||
http://www.gnu.org/software/src-highlite -->
|
||||
<pre><tt><span style="color: #990000">.</span>/dumptbl loader ad5</tt></pre></div></div>
|
||||
<div class="paragraph"><p>We can see a huge chunk of data that is loaded into <code>$e:9000</code>. We’ll
|
||||
go ahead and disassemble it and look for the music player.</p></div>
|
||||
<div class="listingblock">
|
||||
<div class="content"><!-- Generator: GNU source-highlight 3.1.8
|
||||
by Lorenzo Bettini
|
||||
http://www.lorenzobettini.it
|
||||
http://www.gnu.org/software/src-highlite -->
|
||||
<pre><tt><span style="color: #990000">.</span>/decrunch xmasdemo<span style="color: #990000">.</span>2mg <span style="color: #993399">47</span> <span style="color: #993399">92</span> loading
|
||||
<span style="color: #990000">.</span>/disasm loading e9000 <span style="color: #990000">></span> loading<span style="color: #990000">.</span>s</tt></pre></div></div>
|
||||
<div class="paragraph"><p>Most of the loading disassembly is actually noise because we disassembled
|
||||
data.. but we know from the loader that after this block of data is loaded
|
||||
and decrunched, it calls <code>$f:f000</code>. We can use that to follow the code
|
||||
flow and inspect the music player.</p></div>
|
||||
<div class="paragraph"><p>We see that the music player is slightly different from the standard
|
||||
soundsmith music player. The timer runs on the last channel instead of the
|
||||
first channel, and the tempo sets the timer frequency based on a lookup
|
||||
table. Neither of things really matter, they’re just used to lighten the CPU
|
||||
load a bit.</p></div>
|
||||
<div class="paragraph"><p>We discover the music starts at <code>$e:9000</code> and the wavebank starts at <code>$e:e700</code>.
|
||||
We’ll use the trim functions to extract the data into separate files.</p></div>
|
||||
<div class="listingblock">
|
||||
<div class="content"><!-- Generator: GNU source-highlight 3.1.8
|
||||
by Lorenzo Bettini
|
||||
http://www.lorenzobettini.it
|
||||
http://www.gnu.org/software/src-highlite -->
|
||||
<pre><tt><span style="color: #990000">.</span>/trimmusic loading <span style="color: #993399">0</span> loading<span style="color: #990000">.</span>song
|
||||
<span style="color: #990000">.</span>/trimwb loading <span style="color: #993399">5700</span> loading<span style="color: #990000">.</span>wb</tt></pre></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sect1">
|
||||
<h2 id="_main_menu_music">Main Menu Music</h2>
|
||||
<div class="sectionbody">
|
||||
<div class="paragraph"><p>The next time the loader is called, it uses the table at <code>$9ae7</code>.</p></div>
|
||||
<div class="listingblock">
|
||||
<div class="content"><!-- Generator: GNU source-highlight 3.1.8
|
||||
by Lorenzo Bettini
|
||||
http://www.lorenzobettini.it
|
||||
http://www.gnu.org/software/src-highlite -->
|
||||
<pre><tt><span style="color: #990000">.</span>/dumptbl loader ae7</tt></pre></div></div>
|
||||
<div class="paragraph"><p>We hunt for the music player, and discover it. We also discover that
|
||||
the music and wavebank are again combined into a giant block of data.</p></div>
|
||||
<div class="listingblock">
|
||||
<div class="content"><!-- Generator: GNU source-highlight 3.1.8
|
||||
by Lorenzo Bettini
|
||||
http://www.lorenzobettini.it
|
||||
http://www.gnu.org/software/src-highlite -->
|
||||
<pre><tt><span style="color: #990000">.</span>/decrunch xmasdemo<span style="color: #990000">.</span>2mg <span style="color: #993399">139</span> <span style="color: #993399">122</span> main
|
||||
<span style="color: #990000">.</span>/trimmusic main <span style="color: #993399">0</span> main<span style="color: #990000">.</span>song
|
||||
<span style="color: #990000">.</span>/trimwb main <span style="color: #993399">9600</span> main<span style="color: #990000">.</span>wb</tt></pre></div></div>
|
||||
<div class="paragraph"><p>After the main menu, the different parts of the demo are selectable by the
|
||||
user. Making a selection causes the loader to load a unique table which
|
||||
contains the graphics and the music and a different music player.</p></div>
|
||||
<div class="paragraph"><p>The process is pretty much the same for each section. We look at the
|
||||
table, we track down the music player, we use that to determine where
|
||||
the music and wavetables are.</p></div>
|
||||
<div class="paragraph"><p>I’ll just summarize each section from here-on out since the process is the
|
||||
same for each. I will include the dumptbl command for each, though,
|
||||
so you can see the block table for each section.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sect1">
|
||||
<h2 id="_section_1_music">Section 1 Music</h2>
|
||||
<div class="sectionbody">
|
||||
<div class="paragraph"><p>The music and wavebanks are combined again, and loaded into <code>$7:0000</code>.</p></div>
|
||||
<div class="listingblock">
|
||||
<div class="content"><!-- Generator: GNU source-highlight 3.1.8
|
||||
by Lorenzo Bettini
|
||||
http://www.lorenzobettini.it
|
||||
http://www.gnu.org/software/src-highlite -->
|
||||
<pre><tt><span style="color: #990000">.</span>/dumptbl loader b09
|
||||
<span style="color: #990000">.</span>/decrunch xmasdemo<span style="color: #990000">.</span>2mg <span style="color: #993399">393</span> <span style="color: #993399">135</span> section1
|
||||
<span style="color: #990000">.</span>/trimmusic section1 <span style="color: #993399">0</span> section1<span style="color: #990000">.</span>song
|
||||
<span style="color: #990000">.</span>/trimwb section1 a000 section1<span style="color: #990000">.</span>wb</tt></pre></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sect1">
|
||||
<h2 id="_section_2_music">Section 2 Music</h2>
|
||||
<div class="sectionbody">
|
||||
<div class="paragraph"><p>The music is loaded into <code>$5:0000</code>, the wavebank is loaded into <code>$3:0000</code>
|
||||
and not crunched.</p></div>
|
||||
<div class="listingblock">
|
||||
<div class="content"><!-- Generator: GNU source-highlight 3.1.8
|
||||
by Lorenzo Bettini
|
||||
http://www.lorenzobettini.it
|
||||
http://www.gnu.org/software/src-highlite -->
|
||||
<pre><tt><span style="color: #990000">.</span>/dumptbl loader b4d
|
||||
<span style="color: #990000">.</span>/decrunch xmasdemo<span style="color: #990000">.</span>2mg <span style="color: #993399">682</span> <span style="color: #993399">67</span> section2<span style="color: #990000">.</span>song
|
||||
<span style="color: #990000">.</span>/decrunch xmasdemo<span style="color: #990000">.</span>2mg <span style="color: #993399">749</span> <span style="color: #993399">131</span> section2<span style="color: #990000">.</span>wb raw</tt></pre></div></div>
|
||||
<div class="paragraph"><p>You can pass the song and wavebank back through the trim functions to
|
||||
trim off the padding.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sect1">
|
||||
<h2 id="_section_3_music">Section 3 Music</h2>
|
||||
<div class="sectionbody">
|
||||
<div class="paragraph"><p>The music and wavebank are combined again, and loaded into <code>$c:0000</code>.
|
||||
The music doesn’t sound quite right, so I’m thinking it may be patched
|
||||
elsewhere.</p></div>
|
||||
<div class="listingblock">
|
||||
<div class="content"><!-- Generator: GNU source-highlight 3.1.8
|
||||
by Lorenzo Bettini
|
||||
http://www.lorenzobettini.it
|
||||
http://www.gnu.org/software/src-highlite -->
|
||||
<pre><tt><span style="color: #990000">.</span>/dumptbl loader b2b
|
||||
<span style="color: #990000">.</span>/decrunch xmasdemo<span style="color: #990000">.</span>2mg <span style="color: #993399">538</span> <span style="color: #993399">114</span> section3
|
||||
<span style="color: #990000">.</span>/trimmusic section3 <span style="color: #993399">0</span> section3<span style="color: #990000">.</span>song
|
||||
<span style="color: #990000">.</span>/trimwb section3 <span style="color: #993399">8200</span> section3<span style="color: #990000">.</span>wb</tt></pre></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sect1">
|
||||
<h2 id="_section_4_music">Section 4 Music</h2>
|
||||
<div class="sectionbody">
|
||||
<div class="paragraph"><p>The music and wavebank are combined again, loaded into <code>$7:0000</code>.
|
||||
The loader hot-patches the music just after loading it. It sets the word at
|
||||
<code>$7:0006</code> to <code>$3b80</code>. This sets the size of each block to <code>$3b80</code>, where
|
||||
it was previously <code>$3b00</code>. We’ll go ahead and duplicate that patch in
|
||||
our extract process.</p></div>
|
||||
<div class="listingblock">
|
||||
<div class="content"><!-- Generator: GNU source-highlight 3.1.8
|
||||
by Lorenzo Bettini
|
||||
http://www.lorenzobettini.it
|
||||
http://www.gnu.org/software/src-highlite -->
|
||||
<pre><tt><span style="color: #990000">.</span>/dumptbl loader b7f
|
||||
<span style="color: #990000">.</span>/decrunch xmasdemo<span style="color: #990000">.</span>2mg <span style="color: #993399">915</span> <span style="color: #993399">127</span> section4
|
||||
<span style="font-weight: bold"><span style="color: #0000FF">printf</span></span> <span style="color: #FF0000">'</span><span style="color: #CC33CC">\x</span><span style="color: #FF0000">80</span><span style="color: #CC33CC">\x</span><span style="color: #FF0000">3b'</span> <span style="color: #990000">|</span> dd <span style="color: #009900">of</span><span style="color: #990000">=</span>section4 <span style="color: #009900">bs</span><span style="color: #990000">=</span><span style="color: #993399">1</span> <span style="color: #009900">seek</span><span style="color: #990000">=</span><span style="color: #993399">6</span> <span style="color: #009900">count</span><span style="color: #990000">=</span><span style="color: #993399">2</span> <span style="color: #009900">conv</span><span style="color: #990000">=</span>notrunc
|
||||
<span style="color: #990000">.</span>/trimmusic section4 <span style="color: #993399">0</span> section4<span style="color: #990000">.</span>song
|
||||
<span style="color: #990000">.</span>/trimwb section4 b600 section4<span style="color: #990000">.</span>wb</tt></pre></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sect1">
|
||||
<h2 id="_section_5_music">Section 5 Music</h2>
|
||||
<div class="sectionbody">
|
||||
<div class="paragraph"><p>Section 5 doesn’t have any music. This is a very strange section too, since
|
||||
it does two different things depending on whether or not you have the 3rd
|
||||
joystick button held when it launches. Easter egg?</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sect1">
|
||||
<h2 id="_section_6_music">Section 6 Music</h2>
|
||||
<div class="sectionbody">
|
||||
<div class="paragraph"><p>Section 6 also is missing music, but it does have a sound effect that
|
||||
is loaded into <code>$3:0000</code>.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sect1">
|
||||
<h2 id="_section_7_music">Section 7 Music</h2>
|
||||
<div class="sectionbody">
|
||||
<div class="paragraph"><p>The music is loaded into <code>$4:0000</code> along with the demo code. The wavebank
|
||||
is loaded into <code>$3:0000</code>. Unfortunately, the wavebank is incomplete for
|
||||
some reason, making this music unplayable. I haven’t figured out how the
|
||||
demo patches the wavebank.</p></div>
|
||||
<div class="listingblock">
|
||||
<div class="content"><!-- Generator: GNU source-highlight 3.1.8
|
||||
by Lorenzo Bettini
|
||||
http://www.lorenzobettini.it
|
||||
http://www.gnu.org/software/src-highlite -->
|
||||
<pre><tt><span style="color: #990000">.</span>/dumptbl loader bbb
|
||||
<span style="color: #990000">.</span>/decrunch xmasdemo<span style="color: #990000">.</span>2mg <span style="color: #993399">1078</span> <span style="color: #993399">103</span> section7
|
||||
<span style="color: #990000">.</span>/decrunch xmasdemo<span style="color: #990000">.</span>2mg <span style="color: #993399">1181</span> <span style="color: #993399">56</span> section7<span style="color: #990000">.</span>wb raw
|
||||
<span style="color: #990000">.</span>/trimmusic section7 <span style="color: #993399">10000</span> section7<span style="color: #990000">.</span>song</tt></pre></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sect1">
|
||||
<h2 id="_section_8_music">Section 8 Music</h2>
|
||||
<div class="sectionbody">
|
||||
<div class="paragraph"><p>This one is tricky. It first loads the table at <code>$9c1b</code>, which loads
|
||||
uncrunched data into <code>$2000</code>. It then loads the table at <code>$9c2d</code> which
|
||||
loads more uncrunched data into <code>$3:0000</code>. It then takes <code>$100</code> bytes from
|
||||
<code>$2010</code> and appends them onto the end of the data at <code>$3:0000</code>. Finally, it
|
||||
uncrunches the data at <code>$3:0000</code>. So we’ll have to do this patch as well.</p></div>
|
||||
<div class="listingblock">
|
||||
<div class="content"><!-- Generator: GNU source-highlight 3.1.8
|
||||
by Lorenzo Bettini
|
||||
http://www.lorenzobettini.it
|
||||
http://www.gnu.org/software/src-highlite -->
|
||||
<pre><tt><span style="color: #990000">.</span>/dumptbl loader c1b
|
||||
<span style="color: #990000">.</span>/decrunch xmasdemo<span style="color: #990000">.</span>2mg <span style="color: #993399">24</span> <span style="color: #993399">16</span> patch raw
|
||||
<span style="color: #990000">.</span>/dumptbl loader c2d
|
||||
<span style="color: #990000">.</span>/decrunch xmasdemo<span style="color: #990000">.</span>2mg <span style="color: #993399">1487</span> <span style="color: #993399">113</span> crunched raw
|
||||
dd <span style="font-weight: bold"><span style="color: #0000FF">if</span></span><span style="color: #990000">=</span>patch <span style="color: #009900">of</span><span style="color: #990000">=</span>crunched <span style="color: #009900">skip</span><span style="color: #990000">=</span><span style="color: #993399">16</span> <span style="color: #009900">bs</span><span style="color: #990000">=</span><span style="color: #993399">1</span> <span style="color: #009900">count</span><span style="color: #990000">=</span><span style="color: #993399">256</span> <span style="color: #009900">oflag</span><span style="color: #990000">=</span>append <span style="color: #009900">conv</span><span style="color: #990000">=</span>notrunc
|
||||
<span style="color: #990000">.</span>/decrunch crunched <span style="color: #993399">0</span> <span style="color: #993399">0</span> section8
|
||||
<span style="color: #990000">.</span>/trimmusic section8 <span style="color: #993399">1000</span> section8<span style="color: #990000">.</span>song
|
||||
<span style="color: #990000">.</span>/trimwb seciton8 <span style="color: #993399">7000</span> section8<span style="color: #990000">.</span>wb</tt></pre></div></div>
|
||||
<div class="paragraph"><p>And that’s all the music in the xmas demo that I can find.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="footnotes"><hr /></div>
|
||||
<div id="footer">
|
||||
<div id="footer-text">
|
||||
Last updated
|
||||
2017-08-21 11:09:12 MST
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
209
docs/xmas.txt
Normal file
209
docs/xmas.txt
Normal file
@ -0,0 +1,209 @@
|
||||
Extracting music from the Xmas demo
|
||||
===================================
|
||||
|
||||
This is very similar to the documentation on extracting music from Modulae.
|
||||
The steps are a little more involved since the Xmas demo is a multi-part
|
||||
demo, where the code and music for each part is loaded from disk separately.
|
||||
|
||||
As with Modulae, we'll start with extracting the initial boot loader.
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
./disasm xmasdemo.2mg 800 0 1 > boot.s
|
||||
----
|
||||
|
||||
This time, the loading starts at `$08e3`. It loads blocks 7--17 into
|
||||
RAM starting at `$9000` and then calls the decrunch routine at `$0a00`.
|
||||
|
||||
We'll do the same to extract the main loader.
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
./decrunch xmasdemo.2mg 7 11 loader
|
||||
./disasm loader 9000 > loader.s
|
||||
----
|
||||
|
||||
Again, we track down the loading routine. This time, it's a function
|
||||
located at `$9c3f`. It gets called multiple times, to load the different
|
||||
parts of the demo. The `A` register holds the address of the table to use
|
||||
when loading.
|
||||
|
||||
The first time the loader is called, it uses the table at `$9ac3`. This
|
||||
table only contains the loading screen graphics. So we'll ignore it.
|
||||
|
||||
== Loading... Music
|
||||
|
||||
The second time the loader is called, it uses the table at `$9ad5`.
|
||||
We'll extract the table offsets as we did in the Modulae example. Using
|
||||
the relative address of the table from the start of the loader file.
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
./dumptbl loader ad5
|
||||
----
|
||||
|
||||
We can see a huge chunk of data that is loaded into `$e:9000`. We'll
|
||||
go ahead and disassemble it and look for the music player.
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
./decrunch xmasdemo.2mg 47 92 loading
|
||||
./disasm loading e9000 > loading.s
|
||||
----
|
||||
|
||||
Most of the loading disassembly is actually noise because we disassembled
|
||||
data.. but we know from the loader that after this block of data is loaded
|
||||
and decrunched, it calls `$f:f000`. We can use that to follow the code
|
||||
flow and inspect the music player.
|
||||
|
||||
We see that the music player is slightly different from the standard
|
||||
soundsmith music player. The timer runs on the last channel instead of the
|
||||
first channel, and the tempo sets the timer frequency based on a lookup
|
||||
table. Neither of things really matter, they're just used to lighten the CPU
|
||||
load a bit.
|
||||
|
||||
We discover the music starts at `$e:9000` and the wavebank starts at `$e:e700`.
|
||||
We'll use the trim functions to extract the data into separate files.
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
./trimmusic loading 0 loading.song
|
||||
./trimwb loading 5700 loading.wb
|
||||
----
|
||||
|
||||
== Main Menu Music
|
||||
|
||||
The next time the loader is called, it uses the table at `$9ae7`.
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
./dumptbl loader ae7
|
||||
----
|
||||
|
||||
We hunt for the music player, and discover it. We also discover that
|
||||
the music and wavebank are again combined into a giant block of data.
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
./decrunch xmasdemo.2mg 139 122 main
|
||||
./trimmusic main 0 main.song
|
||||
./trimwb main 9600 main.wb
|
||||
----
|
||||
|
||||
After the main menu, the different parts of the demo are selectable by the
|
||||
user. Making a selection causes the loader to load a unique table which
|
||||
contains the graphics and the music and a different music player.
|
||||
|
||||
The process is pretty much the same for each section. We look at the
|
||||
table, we track down the music player, we use that to determine where
|
||||
the music and wavetables are.
|
||||
|
||||
I'll just summarize each section from here-on out since the process is the
|
||||
same for each. I will include the dumptbl command for each, though,
|
||||
so you can see the block table for each section.
|
||||
|
||||
== Section 1 Music
|
||||
|
||||
The music and wavebanks are combined again, and loaded into `$7:0000`.
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
./dumptbl loader b09
|
||||
./decrunch xmasdemo.2mg 393 135 section1
|
||||
./trimmusic section1 0 section1.song
|
||||
./trimwb section1 a000 section1.wb
|
||||
----
|
||||
|
||||
== Section 2 Music
|
||||
|
||||
The music is loaded into `$5:0000`, the wavebank is loaded into `$3:0000`
|
||||
and not crunched.
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
./dumptbl loader b4d
|
||||
./decrunch xmasdemo.2mg 682 67 section2.song
|
||||
./decrunch xmasdemo.2mg 749 131 section2.wb raw
|
||||
----
|
||||
|
||||
You can pass the song and wavebank back through the trim functions to
|
||||
trim off the padding.
|
||||
|
||||
== Section 3 Music
|
||||
|
||||
The music and wavebank are combined again, and loaded into `$c:0000`.
|
||||
The music doesn't sound quite right, so I'm thinking it may be patched
|
||||
elsewhere.
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
./dumptbl loader b2b
|
||||
./decrunch xmasdemo.2mg 538 114 section3
|
||||
./trimmusic section3 0 section3.song
|
||||
./trimwb section3 8200 section3.wb
|
||||
----
|
||||
|
||||
== Section 4 Music
|
||||
|
||||
The music and wavebank are combined again, loaded into `$7:0000`.
|
||||
The loader hot-patches the music just after loading it. It sets the word at
|
||||
`$7:0006` to `$3b80`. This sets the size of each block to `$3b80`, where
|
||||
it was previously `$3b00`. We'll go ahead and duplicate that patch in
|
||||
our extract process.
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
./dumptbl loader b7f
|
||||
./decrunch xmasdemo.2mg 915 127 section4
|
||||
printf '\x80\x3b' | dd of=section4 bs=1 seek=6 count=2 conv=notrunc
|
||||
./trimmusic section4 0 section4.song
|
||||
./trimwb section4 b600 section4.wb
|
||||
----
|
||||
|
||||
== Section 5 Music
|
||||
|
||||
Section 5 doesn't have any music. This is a very strange section too, since
|
||||
it does two different things depending on whether or not you have the 3rd
|
||||
joystick button held when it launches. Easter egg?
|
||||
|
||||
== Section 6 Music
|
||||
|
||||
Section 6 also is missing music, but it does have a sound effect that
|
||||
is loaded into `$3:0000`.
|
||||
|
||||
== Section 7 Music
|
||||
|
||||
The music is loaded into `$4:0000` along with the demo code. The wavebank
|
||||
is loaded into `$3:0000`. Unfortunately, the wavebank is incomplete for
|
||||
some reason, making this music unplayable. I haven't figured out how the
|
||||
demo patches the wavebank.
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
./dumptbl loader bbb
|
||||
./decrunch xmasdemo.2mg 1078 103 section7
|
||||
./decrunch xmasdemo.2mg 1181 56 section7.wb raw
|
||||
./trimmusic section7 10000 section7.song
|
||||
----
|
||||
|
||||
== Section 8 Music
|
||||
|
||||
This one is tricky. It first loads the table at `$9c1b`, which loads
|
||||
uncrunched data into `$2000`. It then loads the table at `$9c2d` which
|
||||
loads more uncrunched data into `$3:0000`. It then takes `$100` bytes from
|
||||
`$2010` and appends them onto the end of the data at `$3:0000`. Finally, it
|
||||
uncrunches the data at `$3:0000`. So we'll have to do this patch as well.
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
./dumptbl loader c1b
|
||||
./decrunch xmasdemo.2mg 24 16 patch raw
|
||||
./dumptbl loader c2d
|
||||
./decrunch xmasdemo.2mg 1487 113 crunched raw
|
||||
dd if=patch of=crunched skip=16 bs=1 count=256 oflag=append conv=notrunc
|
||||
./decrunch crunched 0 0 section8
|
||||
./trimmusic section8 1000 section8.song
|
||||
./trimwb seciton8 7000 section8.wb
|
||||
----
|
||||
|
||||
And that's all the music in the xmas demo that I can find.
|
7
extract/.gitignore
vendored
Normal file
7
extract/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
*.o
|
||||
decrunch
|
||||
dumptbl
|
||||
disasm
|
||||
trimmusic
|
||||
trimwb
|
||||
2mg
|
230
extract/2mg.c
Normal file
230
extract/2mg.c
Normal file
@ -0,0 +1,230 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <ctype.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#define fourcc(x) (x[0] | (x[1] << 8) | (x[2] << 16) | (x[3] << 24))
|
||||
|
||||
static void handleDirectory(uint16_t key, uint8_t *disk, uint32_t diskLen);
|
||||
static void handleEntry(uint8_t *entry, uint8_t *disk, uint32_t diskLen);
|
||||
static void handleFile(uint16_t key, uint32_t len, char *name, uint8_t *disk,
|
||||
uint32_t diskLen, int type);
|
||||
|
||||
static inline uint32_t r32(uint8_t *data) {
|
||||
uint32_t r = *data++;
|
||||
r |= *data++ << 8;
|
||||
r |= *data++ << 16;
|
||||
r |= *data << 24;
|
||||
return r;
|
||||
}
|
||||
|
||||
static inline uint16_t r16(uint8_t *data) {
|
||||
uint16_t r = *data++;
|
||||
r |= *data << 8;
|
||||
return r;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
if (argc != 2) {
|
||||
fprintf(stderr, "Usage: %s <filename.2mg>\n", argv[0]);
|
||||
fprintf(stderr, "This will extract all files in the 2mg into the current folder.\n");
|
||||
fprintf(stderr, "It will create directories as needed.\n");
|
||||
return -1;
|
||||
}
|
||||
FILE *f = fopen(argv[1], "rb");
|
||||
if (!f) {
|
||||
fprintf(stderr, "Couldn't open '%s'\n", argv[1]);
|
||||
return -1;
|
||||
}
|
||||
|
||||
fseek(f, 0, SEEK_END);
|
||||
size_t len = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
if (len < 64) {
|
||||
fprintf(stderr, "%s is not a valid 2mg file\n", argv[1]);
|
||||
fclose(f);
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint8_t *header = malloc(64);
|
||||
fread(header, 64, 1, f);
|
||||
|
||||
if (r32(header) != fourcc("2IMG")) {
|
||||
fprintf(stderr, "%s is not a valid 2mg file\n", argv[1]);
|
||||
fclose(f);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (r32(header + 0xc) != 1) {
|
||||
fprintf(stderr, "Not in ProDOS format\n");
|
||||
fclose(f);
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint32_t diskLen = r32(header + 0x14) * 512;
|
||||
uint32_t diskOfs = r32(header + 0x18);
|
||||
free(header);
|
||||
|
||||
fseek(f, diskOfs, SEEK_SET);
|
||||
uint8_t *disk = malloc(diskLen);
|
||||
fread(disk, diskLen, 1, f);
|
||||
fclose(f);
|
||||
|
||||
handleDirectory(2, disk, diskLen);
|
||||
free(disk);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void readFilename(uint8_t *filename, uint8_t length, char *outname) {
|
||||
for (int i = 0; i < length; i++) {
|
||||
char ch = filename[i];
|
||||
if (isalnum(ch) || ch == '_' || ch == '.' || ch ==' ') {
|
||||
*outname++ = ch;
|
||||
} else {
|
||||
*outname++ = 'x';
|
||||
char hi = ch >> 4;
|
||||
char lo = ch & 0xf;
|
||||
if (hi > 9)
|
||||
*outname++ = 'a' + (hi - 10);
|
||||
else
|
||||
*outname++ = '0' + hi;
|
||||
if (lo > 9)
|
||||
*outname++ = 'a' + (lo - 10);
|
||||
else
|
||||
*outname++ = '0' + lo;
|
||||
}
|
||||
}
|
||||
*outname = 0;
|
||||
}
|
||||
|
||||
static void handleDirectory(uint16_t key, uint8_t *disk, uint32_t diskLen) {
|
||||
uint8_t *block = disk + key * 512;
|
||||
|
||||
if ((block[4] & 0xf0) != 0xf0 && (block[4] & 0xf0) != 0xe0) {
|
||||
fprintf(stderr, "Corrupted directory header\n");
|
||||
return;
|
||||
}
|
||||
|
||||
char dirname[50];
|
||||
readFilename(block + 5, block[4] & 0xf, dirname);
|
||||
|
||||
mkdir(dirname, 0777);
|
||||
chdir(dirname);
|
||||
|
||||
uint8_t entryLength = block[0x23];
|
||||
uint8_t entriesPerBlock= block[0x24];
|
||||
uint16_t fileCount = r16(block + 0x25);
|
||||
uint8_t *entry = block + entryLength + 4;
|
||||
uint8_t curEntry = 1;
|
||||
uint16_t curFile = 0;
|
||||
|
||||
while (curFile < fileCount) {
|
||||
if (entry[0] != 0) {
|
||||
handleEntry(entry, disk, diskLen);
|
||||
curFile++;
|
||||
}
|
||||
curEntry++;
|
||||
entry += entryLength;
|
||||
if (curEntry == entriesPerBlock) {
|
||||
curEntry = 0;
|
||||
block = disk + r16(block + 2) * 512;
|
||||
entry = block + 4;
|
||||
}
|
||||
}
|
||||
|
||||
chdir("..");
|
||||
}
|
||||
|
||||
static void handleEntry(uint8_t *entry, uint8_t *disk, uint32_t diskLen) {
|
||||
uint16_t key = r16(entry + 0x11);
|
||||
uint32_t eof = r32(entry + 0x15) & 0xffffff;
|
||||
|
||||
char filename[50];
|
||||
readFilename(entry + 1, entry[0] & 0xf, filename);
|
||||
|
||||
switch (entry[0] & 0xf0) {
|
||||
case 0x10:
|
||||
handleFile(key, eof, filename, disk, diskLen, 1); // seedling
|
||||
break;
|
||||
case 0x20:
|
||||
handleFile(key, eof, filename, disk, diskLen, 2); // sapling
|
||||
break;
|
||||
case 0x30:
|
||||
handleFile(key, eof, filename, disk, diskLen, 3); // tree
|
||||
break;
|
||||
case 0xd0:
|
||||
handleDirectory(key, disk, diskLen);
|
||||
break;
|
||||
default:
|
||||
fprintf(stderr, "Unknown file type: %x\n", entry[0] >> 4);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static void dumpSeedling(uint8_t *block, uint32_t len, FILE *f) {
|
||||
if (block == NULL)
|
||||
fseek(f, len, SEEK_CUR);
|
||||
else
|
||||
fwrite(block, len, 1, f);
|
||||
}
|
||||
|
||||
static void dumpSapling(uint8_t *index, uint32_t len, FILE *f, uint8_t *disk,
|
||||
uint32_t diskLen) {
|
||||
if (index == NULL) {
|
||||
fseek(f, len, SEEK_CUR);
|
||||
return;
|
||||
}
|
||||
while (len > 0) {
|
||||
uint16_t blockid = index[0] | (index[256] << 8);
|
||||
uint8_t *block = NULL;
|
||||
if (blockid && (blockid + 1) * 512 <= diskLen)
|
||||
block = disk + blockid * 512;
|
||||
uint32_t blen = len > 512 ? 512 : len;
|
||||
dumpSeedling(block, blen, f);
|
||||
len -= blen;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
static void dumpTree(uint8_t *index, uint32_t len, FILE *f, uint8_t *disk,
|
||||
uint32_t diskLen) {
|
||||
if (index == NULL) {
|
||||
fseek(f, len, SEEK_CUR);
|
||||
return;
|
||||
}
|
||||
while (len > 0) {
|
||||
uint16_t blockid = index[0] | (index[256] << 8);
|
||||
uint8_t *block = NULL;
|
||||
if (blockid && (blockid + 1) * 512 <= diskLen)
|
||||
block = disk + blockid * 512;
|
||||
uint32_t blen = len > 256 * 512 ? 256 * 512 : len;
|
||||
dumpSapling(block, blen, f, disk, diskLen);
|
||||
len -= blen;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
static void handleFile(uint16_t key, uint32_t len, char *name, uint8_t *disk,
|
||||
uint32_t diskLen, int type) {
|
||||
uint8_t *block = disk + key * 512;
|
||||
FILE *f = fopen(name, "wb");
|
||||
if (!f) {
|
||||
fprintf(stderr, "Failed to create '%s'\n", name);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 1:
|
||||
dumpSeedling(block, len, f);
|
||||
break;
|
||||
case 2:
|
||||
dumpSapling(block, len, f, disk, diskLen);
|
||||
break;
|
||||
case 3:
|
||||
dumpTree(block, len, f, disk, diskLen);
|
||||
break;
|
||||
}
|
||||
fclose(f);
|
||||
}
|
331
extract/65816.h
Normal file
331
extract/65816.h
Normal file
@ -0,0 +1,331 @@
|
||||
#ifndef __65816_H__
|
||||
#define __65816_H__
|
||||
|
||||
typedef enum {
|
||||
IMP = 0,
|
||||
IMM,
|
||||
IMMM,
|
||||
IMMX,
|
||||
IMMS,
|
||||
ABSO,
|
||||
ABL,
|
||||
ABX,
|
||||
ABY,
|
||||
ABLX,
|
||||
AIX,
|
||||
ZP,
|
||||
ZPX,
|
||||
ZPY,
|
||||
ZPS,
|
||||
IND,
|
||||
INZ,
|
||||
INL,
|
||||
INX,
|
||||
INY,
|
||||
INLY,
|
||||
INS,
|
||||
REL,
|
||||
RELL,
|
||||
BANK,
|
||||
DB,
|
||||
DW,
|
||||
DD
|
||||
} Address;
|
||||
|
||||
typedef struct {
|
||||
const char *inst;
|
||||
Address address;
|
||||
} Opcode;
|
||||
|
||||
static Opcode opcodes[] = {
|
||||
{"brk", IMP}, // 00
|
||||
{"ora", INX}, // 01
|
||||
{"cop", IMP}, // 02
|
||||
{"ora", ZPS}, // 03
|
||||
{"tsb", ZP}, // 04
|
||||
{"ora", ZP}, // 05
|
||||
{"asl", ZP}, // 06
|
||||
{"ora", INL}, // 07
|
||||
{"php", IMP}, // 08
|
||||
{"ora", IMMM}, // 09
|
||||
{"asl", IMP}, // 0a
|
||||
{"phd", IMP}, // 0b
|
||||
{"tsb", ABSO}, // 0c
|
||||
{"ora", ABSO}, // 0d
|
||||
{"asl", ABSO}, // 0e
|
||||
{"ora", ABL}, // 0f
|
||||
{"bpl", REL}, // 10
|
||||
{"ora", INY}, // 11
|
||||
{"ora", INZ}, // 12
|
||||
{"ora", INS}, // 13
|
||||
{"trb", ZP}, // 14
|
||||
{"ora", ZPX}, // 15
|
||||
{"asl", ZPX}, // 16
|
||||
{"ora", INLY}, // 17
|
||||
{"clc", IMP}, // 18
|
||||
{"ora", ABY}, // 19
|
||||
{"inc", IMP}, // 1a
|
||||
{"tcs", IMP}, // 1b
|
||||
{"trb", ABSO}, // 1c
|
||||
{"ora", ABX}, // 1d
|
||||
{"asl", ABX}, // 1e
|
||||
{"ora", ABLX}, // 1f
|
||||
{"jsr", ABSO}, // 20
|
||||
{"and", INX}, // 21
|
||||
{"jsl", ABL}, // 22
|
||||
{"and", ZPS}, // 23
|
||||
{"bit", ZP}, // 24
|
||||
{"and", ZP}, // 25
|
||||
{"rol", ZP}, // 26
|
||||
{"and", INL}, // 27
|
||||
{"plp", IMP}, // 28
|
||||
{"and", IMMM}, // 29
|
||||
{"rol", IMP}, // 2a
|
||||
{"pld", IMP}, // 2b
|
||||
{"bit", ABSO}, // 2c
|
||||
{"and", ABSO}, // 2d
|
||||
{"rol", ABSO}, // 2e
|
||||
{"and", ABL}, // 2f
|
||||
{"bmi", REL}, // 30
|
||||
{"and", INY}, // 31
|
||||
{"and", INZ}, // 32
|
||||
{"and", INS}, // 33
|
||||
{"bit", ZPX}, // 34
|
||||
{"and", ZPX}, // 35
|
||||
{"rol", ZPX}, // 36
|
||||
{"and", INLY}, // 37
|
||||
{"sec", IMP}, // 38
|
||||
{"and", ABY}, // 39
|
||||
{"dec", IMP}, // 3a
|
||||
{"tsc", IMP}, // 3b
|
||||
{"bit", ABX}, // 3c
|
||||
{"and", ABX}, // 3d
|
||||
{"rol", ABX}, // 3e
|
||||
{"and", ABLX}, // 3f
|
||||
{"rti", IMP}, // 40
|
||||
{"eor", INX}, // 41
|
||||
{"db", DB}, // 42
|
||||
{"eor", ZPS}, // 43
|
||||
{"mvp", BANK}, // 44
|
||||
{"eor", ZP}, // 45
|
||||
{"lsr", ZP}, // 46
|
||||
{"eor", INL}, // 47
|
||||
{"pha", IMP}, // 48
|
||||
{"eor", IMMM}, // 49
|
||||
{"lsr", IMP}, // 4a
|
||||
{"phk", IMP}, // 4b
|
||||
{"jmp", ABSO}, // 4c
|
||||
{"eor", ABSO}, // 4d
|
||||
{"lsr", ABSO}, // 4e
|
||||
{"eor", ABL}, // 4f
|
||||
{"bvc", REL}, // 50
|
||||
{"eor", INY}, // 51
|
||||
{"eor", INZ}, // 52
|
||||
{"eor", INS}, // 53
|
||||
{"mvn", BANK}, // 54
|
||||
{"eor", ZPX}, // 55
|
||||
{"lsr", ZPX}, // 56
|
||||
{"eor", INLY}, // 57
|
||||
{"cli", IMP}, // 58
|
||||
{"eor", ABY}, // 59
|
||||
{"phy", IMP}, // 5a
|
||||
{"tcd", IMP}, // 5b
|
||||
{"jmp", ABL}, // 5c
|
||||
{"eor", ABX}, // 5d
|
||||
{"lsr", ABX}, // 5e
|
||||
{"eor", ABLX}, // 5f
|
||||
{"rts", IMP}, // 60
|
||||
{"adc", INX}, // 61
|
||||
{"per", IMP}, // 62
|
||||
{"adc", ZPS}, // 63
|
||||
{"stz", ZP}, // 64
|
||||
{"adc", ZP}, // 65
|
||||
{"ror", ZP}, // 66
|
||||
{"adc", INL}, // 67
|
||||
{"pla", IMP}, // 68
|
||||
{"adc", IMMM}, // 69
|
||||
{"ror", IMP}, // 6a
|
||||
{"rtl", IMP}, // 6b
|
||||
{"jmp", IND}, // 6c
|
||||
{"adc", ABSO}, // 6d
|
||||
{"ror", ABSO}, // 6e
|
||||
{"adc", ABL}, // 6f
|
||||
{"bvs", REL}, // 70
|
||||
{"adc", INY}, // 71
|
||||
{"adc", INZ}, // 72
|
||||
{"adc", INS}, // 73
|
||||
{"stz", ZPX}, // 74
|
||||
{"adc", ZPX}, // 75
|
||||
{"ror", ZPX}, // 76
|
||||
{"adc", INLY}, // 77
|
||||
{"sei", IMP}, // 78
|
||||
{"adc", ABY}, // 79
|
||||
{"ply", IMP}, // 7a
|
||||
{"tdc", IMP}, // 7b
|
||||
{"jmp", AIX}, // 7c
|
||||
{"adc", ABX}, // 7d
|
||||
{"ror", ABX}, // 7e
|
||||
{"adc", ABLX}, // 7f
|
||||
{"bra", REL}, // 80
|
||||
{"sta", INX}, // 81
|
||||
{"brl", RELL}, // 82
|
||||
{"sta", ZPS}, // 83
|
||||
{"sty", ZP}, // 84
|
||||
{"sta", ZP}, // 85
|
||||
{"stx", ZP}, // 86
|
||||
{"sta", INL}, // 87
|
||||
{"dey", IMP}, // 88
|
||||
{"bit", IMMM}, // 89
|
||||
{"txa", IMP}, // 8a
|
||||
{"phb", IMP}, // 8b
|
||||
{"sty", ABSO}, // 8c
|
||||
{"sta", ABSO}, // 8d
|
||||
{"stx", ABSO}, // 8e
|
||||
{"sta", ABL}, // 8f
|
||||
{"bcc", REL}, // 90
|
||||
{"sta", INY}, // 91
|
||||
{"sta", INZ}, // 92
|
||||
{"sta", INS}, // 93
|
||||
{"sty", ZPX}, // 94
|
||||
{"sta", ZPX}, // 95
|
||||
{"stx", ZPY}, // 96
|
||||
{"sta", INLY}, // 97
|
||||
{"tya", IMP}, // 98
|
||||
{"sta", ABY}, // 99
|
||||
{"txs", IMP}, // 9a
|
||||
{"txy", IMP}, // 9b
|
||||
{"stz", ABSO}, // 9c
|
||||
{"sta", ABX}, // 9d
|
||||
{"stz", ABX}, // 9e
|
||||
{"sta", ABLX}, // 9f
|
||||
{"ldy", IMMX}, // a0
|
||||
{"lda", INX}, // a1
|
||||
{"ldx", IMMX}, // a2
|
||||
{"lda", ZPS}, // a3
|
||||
{"ldy", ZP}, // a4
|
||||
{"lda", ZP}, // a5
|
||||
{"ldx", ZP}, // a6
|
||||
{"lda", INL}, // a7
|
||||
{"tay", IMP}, // a8
|
||||
{"lda", IMMM}, // a9
|
||||
{"tax", IMP}, // aa
|
||||
{"plb", IMP}, // ab
|
||||
{"ldy", ABSO}, // ac
|
||||
{"lda", ABSO}, // ad
|
||||
{"ldx", ABSO}, // ae
|
||||
{"lda", ABL}, // af
|
||||
{"bcs", REL}, // b0
|
||||
{"lda", INY}, // b1
|
||||
{"lda", INZ}, // b2
|
||||
{"lda", INS}, // b3
|
||||
{"ldy", ZPX}, // b4
|
||||
{"lda", ZPX}, // b5
|
||||
{"ldx", ZPY}, // b6
|
||||
{"lda", INLY}, // b7
|
||||
{"clv", IMP}, // b8
|
||||
{"lda", ABY}, // b9
|
||||
{"tsx", IMP}, // ba
|
||||
{"tyx", IMP}, // bb
|
||||
{"ldy", ABX}, // bc
|
||||
{"lda", ABX}, // bd
|
||||
{"ldx", ABY}, // be
|
||||
{"lda", ABLX}, // bf
|
||||
{"cpy", IMMX}, // c0
|
||||
{"cmp", INX}, // c1
|
||||
{"rep", IMM}, // c2
|
||||
{"cmp", ZPS}, // c3
|
||||
{"cpy", ZP}, // c4
|
||||
{"cmp", ZP}, // c5
|
||||
{"dec", ZP}, // c6
|
||||
{"cmp", INL}, // c7
|
||||
{"iny", IMP}, // c8
|
||||
{"cmp", IMMM}, // c9
|
||||
{"dex", IMP}, // ca
|
||||
{"wai", IMP}, // cb
|
||||
{"cpy", ABSO}, // cc
|
||||
{"cmp", ABSO}, // cd
|
||||
{"dec", ABSO}, // ce
|
||||
{"cmp", ABL}, // cf
|
||||
{"bne", REL}, // d0
|
||||
{"cmp", INY}, // d1
|
||||
{"cmp", INZ}, // d2
|
||||
{"cmp", INS}, // d3
|
||||
{"pei", IMP}, // d4
|
||||
{"cmp", ZPX}, // d5
|
||||
{"dec", ZPX}, // d6
|
||||
{"cmp", INLY}, // d7
|
||||
{"cld", IMP}, // d8
|
||||
{"cmp", ABY}, // d9
|
||||
{"phx", IMP}, // da
|
||||
{"stp", IMP}, // db
|
||||
{"jmp", IND}, // dc
|
||||
{"cmp", ABX}, // dd
|
||||
{"dec", ABX}, // de
|
||||
{"cmp", ABLX}, // df
|
||||
{"cpx", IMMX}, // e0
|
||||
{"sbc", INX}, // e1
|
||||
{"sep", IMM}, // e2
|
||||
{"sbc", ZPS}, // e3
|
||||
{"cpx", ZP}, // e4
|
||||
{"sbc", ZP}, // e5
|
||||
{"inc", ZP}, // e6
|
||||
{"sbc", INL}, // e7
|
||||
{"inx", IMP}, // e8
|
||||
{"sbc", IMMM}, // e9
|
||||
{"nop", IMP}, // ea
|
||||
{"xba", IMP}, // eb
|
||||
{"cpx", ABSO}, // ec
|
||||
{"sbc", ABSO}, // ed
|
||||
{"inc", ABSO}, // ee
|
||||
{"sbc", ABL}, // ef
|
||||
{"beq", REL}, // f0
|
||||
{"sbc", INY}, // f1
|
||||
{"sbc", INZ}, // f2
|
||||
{"sbc", INS}, // f3
|
||||
{"pea", IMMS}, // f4
|
||||
{"sbc", ZPX}, // f5
|
||||
{"inc", ZPX}, // f6
|
||||
{"sbc", INLY}, // f7
|
||||
{"sed", IMP}, // f8
|
||||
{"sbc", ABY}, // f9
|
||||
{"plx", IMP}, // fa
|
||||
{"xce", IMP}, // fb
|
||||
{"jsr", AIX}, // fc
|
||||
{"sbc", ABX}, // fd
|
||||
{"inc", ABX}, // fe
|
||||
{"sbc", ABLX} // ff
|
||||
};
|
||||
|
||||
|
||||
uint8_t addressSizes[] = {
|
||||
1, // IMP
|
||||
2, // IMM
|
||||
3, // IMMM
|
||||
3, // IMMX
|
||||
3, // IMMS
|
||||
3, // ABSO
|
||||
4, // ABL
|
||||
3, // ABX
|
||||
3, // ABY
|
||||
4, // ABLX
|
||||
3, // AIX
|
||||
2, // ZP
|
||||
2, // ZPX
|
||||
2, // ZPY
|
||||
2, // ZPS
|
||||
3, // IND
|
||||
2, // INZ
|
||||
2, // INL
|
||||
2, // INX
|
||||
2, // INY
|
||||
2, // INLY
|
||||
2, // INS
|
||||
2, // REL
|
||||
3, // RELL
|
||||
3, // BANK
|
||||
1, // DB
|
||||
2, // DW
|
||||
4 // DD
|
||||
};
|
||||
|
||||
#endif
|
28
extract/Makefile
Normal file
28
extract/Makefile
Normal file
@ -0,0 +1,28 @@
|
||||
CC=clang
|
||||
CFLAGS=-Wall
|
||||
|
||||
all: decrunch disasm dumptbl trimmusic trimwb 2mg
|
||||
|
||||
decrunch: decrunch.o
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
disasm: disasm.o
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
dumptbl: dumptbl.o
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
trimmusic: trimmusic.o
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
trimwb: trimwb.o
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
2mg: 2mg.o
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
%.o: %.c
|
||||
$(CC) -c $(CFLAGS) -o $@ $<
|
||||
|
||||
clean:
|
||||
rm -f *.o decrunch disasm dumptbl trimmusic trimwb 2mg
|
47
extract/README.md
Normal file
47
extract/README.md
Normal file
@ -0,0 +1,47 @@
|
||||
This directory contains utiltiies I wrote to extract music from
|
||||
FTA demos.
|
||||
|
||||
Extracting the music is a multistep process, that involves analyzing
|
||||
boot loaders to determine where on disk the music is stored (since the
|
||||
FTA demos don't have filesystems).
|
||||
|
||||
Compile all the utilities by running `make`.
|
||||
|
||||
There are step-by-step tutorials on how to use these tools to extract music from
|
||||
the various FTA demos inside the docs/ folder.
|
||||
|
||||
Most of these tools work on 2mg images, and are controlled at a 512-byte block level.
|
||||
|
||||
== disasm
|
||||
|
||||
`disasm` is a quick and dirty 65816 disassembler. You specify the starting
|
||||
block and the number of blocks to disassemble. You also specify the starting address (in hex)
|
||||
for disassembly.
|
||||
into memory starting at 0x800. Then redirect the disassembly into a file called boot.s
|
||||
|
||||
== decrunch
|
||||
|
||||
`decrunch` will extract. There's a special `raw` keyword that will skip the
|
||||
decrunching routine, and thus this routine can also be used to extract random blocks
|
||||
out of a disk image.
|
||||
|
||||
== dumptbl
|
||||
|
||||
Modulae and the Xmas demo both use a loader that uses tables of blocks and their
|
||||
destination addresses to load data off of the disk. This tool will parse and
|
||||
print out those tables.
|
||||
|
||||
== trimmusic and trimwb
|
||||
|
||||
Since the music and wavebanks weren't loaded from files, but instead just loaded
|
||||
as groups of 512-byte blocks from disk. They should be trimmed to the correct
|
||||
size afterwards. These routines will determine the proper length of the files
|
||||
and trim them. Since these files don't necessarily start at the beginning of
|
||||
the block, you can also specify the starting offset of the song or music inside
|
||||
the block.
|
||||
|
||||
== 2mg
|
||||
|
||||
This tool is for the disk images that have an actual ProDOS filesystem on them.
|
||||
Simply pass it a disk image and it will extract the entire disk image, creating
|
||||
directories as needed.
|
410
extract/addresses.h
Normal file
410
extract/addresses.h
Normal file
@ -0,0 +1,410 @@
|
||||
#ifndef __ADDRESSES_H__
|
||||
#define __ADDRESSES_H__
|
||||
|
||||
typedef struct {
|
||||
uint32_t address;
|
||||
const char *comment;
|
||||
} MemAddress;
|
||||
|
||||
static MemAddress addresses[] = {
|
||||
{0x03d0, "Enter BASIC"},
|
||||
{0x03d2, "Reconnect DOS"},
|
||||
{0x03d9, "Cow Sound"},
|
||||
{0x03ea, "Reconnect IO"},
|
||||
{0x03f2, "Control-Reset Vector"},
|
||||
{0x03f5, "Ampersand Vector"},
|
||||
{0x03f8, "Control-Y Vector"},
|
||||
{0x0400, "Text Screen"},
|
||||
{0x0800, "Text Screen 2"},
|
||||
{0x0803, "Enter assembler"},
|
||||
{0x2000, "Hires screen"},
|
||||
{0x4000, "Hires screen 2"},
|
||||
{0x9dbf, "Reconnect DOS 3.3"},
|
||||
{0xa56e, "CATALOG"},
|
||||
{0xc000, "KBD / 80STOREOFF"},
|
||||
{0xc001, "80STOREON"},
|
||||
{0xc002, "RDMAINRAM"},
|
||||
{0xc003, "RDCARDRAM"},
|
||||
{0xc004, "WRMAINRAM"},
|
||||
{0xc005, "WRCARDRAM"},
|
||||
{0xc006, "SETSLOTCXROM"},
|
||||
{0xc007, "SETINTCXROM"},
|
||||
{0xc008, "SETSTDZP"},
|
||||
{0xc009, "SETALTZP"},
|
||||
{0xc00a, "SETINTC3ROM"},
|
||||
{0xc00b, "SETSLOTC3ROM"},
|
||||
{0xc00c, "CLR80VID"},
|
||||
{0xc00d, "SET80VID"},
|
||||
{0xc00e, "CLRALTCHAR"},
|
||||
{0xc00f, "SETALTCHAR"},
|
||||
{0xc010, "KBDSTRB"},
|
||||
{0xc011, "RDLCBNK2"},
|
||||
{0xc012, "RDLCRAM"},
|
||||
{0xc013, "RDRAMRD"},
|
||||
{0xc014, "RDRAMWRT"},
|
||||
{0xc015, "RDCXROM"},
|
||||
{0xc016, "RDALTZP"},
|
||||
{0xc017, "RDC3ROM"},
|
||||
{0xc018, "RD80STORE"},
|
||||
{0xc019, "RDVBL"},
|
||||
{0xc01a, "RDTEXT"},
|
||||
{0xc01b, "RDMIXED"},
|
||||
{0xc01c, "RDPAGE2"},
|
||||
{0xc01d, "RDHIRES"},
|
||||
{0xc01e, "RDALTCHAR"},
|
||||
{0xc01f, "RD80VID"},
|
||||
{0xc020, "TAPEOUT"},
|
||||
{0xc021, "MONOCOLOR"},
|
||||
{0xc022, "TBCOLOR"},
|
||||
{0xc023, "VGCINT"},
|
||||
{0xc024, "MOUSEDATA"},
|
||||
{0xc025, "KEYMODREG"},
|
||||
{0xc026, "DATAREG"},
|
||||
{0xc027, "KMSTATUS"},
|
||||
{0xc028, "ROMBANK"},
|
||||
{0xc029, "NEWVIDEO"},
|
||||
{0xc02b, "LANGSEL"},
|
||||
{0xc02c, "CHARROM"},
|
||||
{0xc02d, "SLTROMSEL"},
|
||||
{0xc02e, "VERTCNT"},
|
||||
{0xc02f, "HORIZCNT"},
|
||||
{0xc030, "SPKR"},
|
||||
{0xc031, "DISKREG"},
|
||||
{0xc032, "SCANINT"},
|
||||
{0xc033, "CLOCKDATA"},
|
||||
{0xc034, "CLOCKCTL"},
|
||||
{0xc035, "SHADOW"},
|
||||
{0xc036, "CYAREG"},
|
||||
{0xc037, "DMAREG"},
|
||||
{0xc038, "SCCBREG"},
|
||||
{0xc039, "SCCAREG"},
|
||||
{0xc03a, "SCCBDATA"},
|
||||
{0xc03b, "SCCADATA"},
|
||||
{0xc03c, "SOUNDCTL"},
|
||||
{0xc03d, "SOUNDDATA"},
|
||||
{0xc03e, "SOUNDADRL"},
|
||||
{0xc03f, "SOUNDADRH"},
|
||||
{0xc040, "STROBE"},
|
||||
{0xc041, "INTEN"},
|
||||
{0xc044, "MMDELTAX"},
|
||||
{0xc045, "MMDELTAY"},
|
||||
{0xc046, "DIAGTYPE"},
|
||||
{0xc047, "CLRVBLINT"},
|
||||
{0xc048, "CLRXYINT"},
|
||||
{0xc050, "TXTCLR"},
|
||||
{0xc051, "TXTSET"},
|
||||
{0xc052, "MIXCLR"},
|
||||
{0xc053, "MIXSET"},
|
||||
{0xc054, "TXTPAGE1"},
|
||||
{0xc055, "TXTPAGE2"},
|
||||
{0xc056, "LORES"},
|
||||
{0xc057, "HIRES"},
|
||||
{0xc058, "CLRAN0"},
|
||||
{0xc059, "SETAN0"},
|
||||
{0xc05a, "CLRAN1"},
|
||||
{0xc05b, "SETAN1"},
|
||||
{0xc05c, "CLRAN2"},
|
||||
{0xc05d, "SETAN2"},
|
||||
{0xc05e, "DHIRESON"},
|
||||
{0xc05f, "DHIRESOFF"},
|
||||
{0xc060, "TAPEIN"},
|
||||
{0xc061, "RDBTN0"},
|
||||
{0xc062, "RDBTN1"},
|
||||
{0xc063, "RDBTN2"},
|
||||
{0xc064, "PADDL0"},
|
||||
{0xc065, "PADDL1"},
|
||||
{0xc066, "PADDL2"},
|
||||
{0xc067, "PADDL3"},
|
||||
{0xc068, "STATEREG"},
|
||||
{0xc06d, "TESTREG"},
|
||||
{0xc06e, "CLTRM"},
|
||||
{0xc06f, "ENTM"},
|
||||
{0xc070, "PTRIG"},
|
||||
{0xc073, "BANKSEL"},
|
||||
{0xc07e, "IOUDISON"},
|
||||
{0xc07f, "IOUDISOFF"},
|
||||
{0xc081, "ROMIN"},
|
||||
{0xc083, "LCBANK2"},
|
||||
{0xc08b, "LCBANK1"},
|
||||
{0xc0e0, "PH0 off"},
|
||||
{0xc0e1, "PH0 on"},
|
||||
{0xc0e2, "PH1 off"},
|
||||
{0xc0e3, "PH1 on"},
|
||||
{0xc0e4, "PH2 off"},
|
||||
{0xc0e5, "PH2 on"},
|
||||
{0xc0e6, "PH3 off"},
|
||||
{0xc0e7, "PH3 on"},
|
||||
{0xc0e8, "motor off"},
|
||||
{0xc0e9, "motor on"},
|
||||
{0xc0ea, "drive 1"},
|
||||
{0xc0eb, "drive 2"},
|
||||
{0xc0ec, "q6 off"},
|
||||
{0xc0ed, "q6 on"},
|
||||
{0xc0ee, "q7 off"},
|
||||
{0xc0ef, "q7 on"},
|
||||
{0xc311, "AUXMOVE"},
|
||||
{0xc314, "XFER"},
|
||||
{0xc50d, "Smartport"},
|
||||
{0xc70d, "Smartport"},
|
||||
{0xcfff," CLRROM"},
|
||||
{0xd1fc, "Hires Find"},
|
||||
{0xd2c9, "Hires bg"},
|
||||
{0xd331, "Hires graphics bg"},
|
||||
{0xd33a, "Hires DRAW1"},
|
||||
{0xd3b9, "Hires SHLOAD"},
|
||||
{0xd683, "Clear FOR"},
|
||||
{0xdafb, "Carriage Return"},
|
||||
{0xe000, "Reset Int Basic"},
|
||||
{0xe04b, "IntBASIC LIST"},
|
||||
{0xe5ad, "NEW"},
|
||||
{0xe5b7, "PLOT"},
|
||||
{0xe836, "IntBASIC CHAIN"},
|
||||
{0xefec, "IntBASIC RUN"},
|
||||
{0xf07c, "IntBASIC LOAD"},
|
||||
{0xf0e0, "Leave monitor"},
|
||||
{0xf123, "DRAW shape"},
|
||||
{0xf14f, "Plot point"},
|
||||
{0xf171, "IntBASIC TRACE ON"},
|
||||
{0xf176, "IntBASIC TRACE OFF"},
|
||||
{0xf30a, "IntBASIC CON"},
|
||||
{0xf317, "RESUME"},
|
||||
{0xf328, "Clear error"},
|
||||
{0xf3de, "HGR"},
|
||||
{0xf3e4, "Show hires"},
|
||||
{0xf3f2, "Clear hires"},
|
||||
{0xf3f6, "Clear hires color"},
|
||||
{0xf666, "Enter assembler"},
|
||||
{0xf800, "PLOT"},
|
||||
{0xf80e, "PLOT1"},
|
||||
{0xf819, "HLINE"},
|
||||
{0xf828, "VLINE"},
|
||||
{0xf832, "CLRSCR"},
|
||||
{0xf836, "CLRTOP"},
|
||||
{0xf838, "Clear lores y"},
|
||||
{0xf83c, "Clear rect"},
|
||||
{0xf847, "GBASCALC"},
|
||||
{0xf85e, "Add 3 COLOR"},
|
||||
{0xf85f, "NXTCOL"},
|
||||
{0xf864, "SETCOL"},
|
||||
{0xf871, "SCRN"},
|
||||
{0xf88c, "INSDS1.2"},
|
||||
{0xf88e, "INSDS2"},
|
||||
{0xf890, "GET816LEN"},
|
||||
{0xf8d0, "INSTDSP"},
|
||||
{0xf940, "PRNTYX"},
|
||||
{0xf941, "PRNTAX"},
|
||||
{0xf944, "PRNTX"},
|
||||
{0xf948, "PRBLNK"},
|
||||
{0xf94a, "PRBL2"},
|
||||
{0xf94c, "Print X blank"},
|
||||
{0xf953, "PCADJ"},
|
||||
{0xf962, "TEXT2COPY"},
|
||||
{0xfa40, "OLDIRQ"},
|
||||
{0xfa4c, "BREAK"},
|
||||
{0xfa59, "OLDBRK"},
|
||||
{0xfa62, "RESET"},
|
||||
{0xfaa6, "PWRUP"},
|
||||
{0xfaba, "SLOOP"},
|
||||
{0xfad7, "REGDSP"},
|
||||
{0xfb19, "RTBL"},
|
||||
{0xfb1e, "PREAD"},
|
||||
{0xfb21, "PREAD4"},
|
||||
{0xfb2f, "INIT"},
|
||||
{0xfb39, "SETTXT"},
|
||||
{0xfb40, "SETGR"},
|
||||
{0xfb4b, "SETWND"},
|
||||
{0xfb51, "SETWND2"},
|
||||
{0xfb5b, "TABV"},
|
||||
{0xfb60, "APPLEII"},
|
||||
{0xfb6f, "SETPWRC"},
|
||||
{0xfb78, "VIDWAIT"},
|
||||
{0xfb88, "KBDWAIT"},
|
||||
{0xfbb3, "VERSION"},
|
||||
{0xfbbf, "ZIDBYTE2"},
|
||||
{0xfbc0, "ZIDBYTE"},
|
||||
{0xfbc1, "BASCALC"},
|
||||
{0xfbdd, "BELL1"},
|
||||
{0xfbe2, "BELL1.2"},
|
||||
{0xfbe4, "BELL2"},
|
||||
{0xfbf0, "STORADV"},
|
||||
{0xfbf4, "ADVANCE"},
|
||||
{0xfbfd, "VIDOUT"},
|
||||
{0xfc10, "BS"},
|
||||
{0xfc1a, "UP"},
|
||||
{0xfc22, "VTAB"},
|
||||
{0xfc24, "VTABZ"},
|
||||
{0xfc2c, "ESC"},
|
||||
{0xfc42, "CLREOP"},
|
||||
{0xfc58, "HOME"},
|
||||
{0xfc62, "CR"},
|
||||
{0xfc66, "LF"},
|
||||
{0xfc70, "SCROLL"},
|
||||
{0xfc9c, "CLREOL"},
|
||||
{0xfc9e, "CLREOLZ"},
|
||||
{0xfca8, "WAIT"},
|
||||
{0xfcb4, "NXTA4"},
|
||||
{0xfcba, "NXTA1"},
|
||||
{0xfcc9, "HEADR"},
|
||||
{0xfd0c, "RDKEY"},
|
||||
{0xfd10, "FD10"},
|
||||
{0xfd18, "RDKEY1"},
|
||||
{0xfd1b, "KEYIN"},
|
||||
{0xfd35, "RDCHAR"},
|
||||
{0xfd5a, "Wait return"},
|
||||
{0xfd5c, "Ring bell wait"},
|
||||
{0xfd67, "GETLNZ"},
|
||||
{0xfd6a, "GETLN"},
|
||||
{0xfd6c, "GETLN0"},
|
||||
{0xfd6f, "GETLN1"},
|
||||
{0xfd75, "Wait line"},
|
||||
{0xfd8b, "CROUT1"},
|
||||
{0xfd8e, "CROUT"},
|
||||
{0xfd92, "PRA1"},
|
||||
{0xfda3, "Print memory"},
|
||||
{0xfdda, "PRBYTE"},
|
||||
{0xfde3, "PRHEX"},
|
||||
{0xfded, "COUT"},
|
||||
{0xfdf0, "COUT1"},
|
||||
{0xfdf6, "COUTZ"},
|
||||
{0xfe1f, "IDROUTINE"},
|
||||
{0xfe2c, "MOVE"},
|
||||
{0xfe5e, "LIST"},
|
||||
{0xfe61, "Disassembler"},
|
||||
{0xfe80, "INVERSE"},
|
||||
{0xfe84, "NORMAL"},
|
||||
{0xfe86, "Set I"},
|
||||
{0xfe89, "SETKBD"},
|
||||
{0xfe8b, "INPORT"},
|
||||
{0xfe93, "SETVID"},
|
||||
{0xfe95, "OUTPORT"},
|
||||
{0xfeb0, "Jump BASIC"},
|
||||
{0xfeb6, "GO"},
|
||||
{0xfebf, "Display regs"},
|
||||
{0xfec2, "Perform trace"},
|
||||
{0xfecd, "WRITE"},
|
||||
{0xfefd, "READ"},
|
||||
{0xff2d, "PRERR"},
|
||||
{0xff3a, "BELL"},
|
||||
{0xff3f, "RESTORE"},
|
||||
{0xff44, "RSTR1"},
|
||||
{0xff4a, "SAVE"},
|
||||
{0xff4c, "SAV1"},
|
||||
{0xff58, "IORTS"},
|
||||
{0xff59, "OLDRST"},
|
||||
{0xff65, "MON"},
|
||||
{0xff69, "MONZ"},
|
||||
{0xff6c, "MONZ2"},
|
||||
{0xff70, "MONZ4"},
|
||||
{0xff8a, "DIG"},
|
||||
{0xffa7, "GETNUM"},
|
||||
{0xffad, "NXTCHR"},
|
||||
{0xffbe, "TOSUB"},
|
||||
{0xffc7, "ZMODE"},
|
||||
{0xe01e04, "StdText"},
|
||||
{0xe01e08, "StdLine"},
|
||||
{0xe01e0c, "StdRect"},
|
||||
{0xe01e10, "StdRRect"},
|
||||
{0xe01e14, "StdOval"},
|
||||
{0xe01e18, "StdArc"},
|
||||
{0xe01e1c, "StdPoly"},
|
||||
{0xe01e20, "StdRgn"},
|
||||
{0xe01e24, "StdPixels"},
|
||||
{0xe01e28, "StdComment"},
|
||||
{0xe01e2c, "StdTxMeas"},
|
||||
{0xe01e30, "StdTxBnds"},
|
||||
{0xe01e34, "StdGetPic"},
|
||||
{0xe01e38, "StdPutPic"},
|
||||
{0xe01e98, "ShieldCursor"},
|
||||
{0xe01e9c, "UnshieldCursor"},
|
||||
{0xe10000, "System Tool dispatch"},
|
||||
{0xe10004, "System Tool dispatch"},
|
||||
{0xe10008, "User Tool dispatch"},
|
||||
{0xe1000c, "User Tool dispatch"},
|
||||
{0xe10010, "Interrupt manager"},
|
||||
{0xe10014, "COP manager"},
|
||||
{0xe10018, "Abort manager"},
|
||||
{0xe1001c, "System death manager"},
|
||||
{0xe10020, "AppleTalk interrupt"},
|
||||
{0xe10024, "Serial interrupt"},
|
||||
{0xe10028, "Scanline interrupt"},
|
||||
{0xe1002c, "Sound interrupt"},
|
||||
{0xe10030, "VBlank interrupt"},
|
||||
{0xe10034, "Mouse interrupt"},
|
||||
{0xe10038, "250ms interrupt"},
|
||||
{0xe1003c, "Keyboard interrupt"},
|
||||
{0xe10040, "ADB Response"},
|
||||
{0xe10044, "ADB SRQ"},
|
||||
{0xe10048, "DA manager"},
|
||||
{0xe1004c, "Flush Buffer"},
|
||||
{0xe10050, "KbdMicro interrupt"},
|
||||
{0xe10054, "1s interrupt"},
|
||||
{0xe10058, "External VGC interrupt"},
|
||||
{0xe1005c, "Ohter interrupt"},
|
||||
{0xe10060, "Cursor update"},
|
||||
{0xe10064, "IncBusy"},
|
||||
{0xe10068, "DecBusy"},
|
||||
{0xe1006c, "Bell vector"},
|
||||
{0xe10070, "Break vector"},
|
||||
{0xe10074, "Trace vector"},
|
||||
{0xe10078, "Step vector"},
|
||||
{0xe1007c, "ROM disk"},
|
||||
{0xe10080, "ToWriteBram"},
|
||||
{0xe10084, "ToReadBram"},
|
||||
{0xe10088, "ToWriteTime"},
|
||||
{0xe1008c, "ToReadTime"},
|
||||
{0xe10090, "ToCtrlPanel"},
|
||||
{0xe10094, "ToBramSetup"},
|
||||
{0xe10098, "ToPrintMsg8"},
|
||||
{0xe1009c, "ToPrintMsg16"},
|
||||
{0xe100a0, "Native Ctl-Y"},
|
||||
{0xe100a4, "ToAltDispCDA"},
|
||||
{0xe100a8, "Prodos 16"},
|
||||
{0xe100ac, "OS vector"},
|
||||
{0xe100b0, "GS/OS"},
|
||||
{0xe100b4, "P8 Switch"},
|
||||
{0xe100b8, "Public Flags"},
|
||||
{0xe100bc, "OS Kind"},
|
||||
{0xe100bd, "OS Boot"},
|
||||
{0xe100be, "OS Busy"},
|
||||
{0xe100c0, "MsgPtr"},
|
||||
{0xe10180, "ToBusyStrip"},
|
||||
{0xe10184, "ToStrip"},
|
||||
{0xe101b2, "MidiInputPoll"},
|
||||
{0xe10200, "Memory manager"},
|
||||
{0xe10204, "Set System Speed"},
|
||||
{0xe10208, "Slot Arbiter"},
|
||||
{0xe10220, "Hypercard callback"},
|
||||
{0xe10224, "WordForRTL"},
|
||||
{0xe11004, "ATLK Basic"},
|
||||
{0xe11008, "ATLK Pascal"},
|
||||
{0xe1100c, "ATLK RamGoComp"},
|
||||
{0xe11010, "ATLK SoftReset"},
|
||||
{0xe11014, "ATLK RamDispatch"},
|
||||
{0xe11018, "ATLK RamForbid"},
|
||||
{0xe1101c, "ATLK RamPermit"},
|
||||
{0xe11020, "ATLK ProEntry"},
|
||||
{0xe11022, "ATLK ProDOS"},
|
||||
{0xe11026, "ATLK SerStatus"},
|
||||
{0xe1102a, "ATLK SerWrite"},
|
||||
{0xe1102e, "ATLK SerRead"},
|
||||
{0xe1103e, "ATLK PFI"},
|
||||
{0xe1d600, "ATLK CmdTable"},
|
||||
{0xe1da00, "ATLK TickCount"}
|
||||
};
|
||||
|
||||
#define numAddresses (sizeof(addresses) / sizeof(addresses[0]))
|
||||
|
||||
static const char *addressLookup(uint32_t addr) {
|
||||
for (int i = 0; i < numAddresses; i++) {
|
||||
if (addresses[i].address >= addr) {
|
||||
if (addresses[i].address == addr)
|
||||
return addresses[i].comment;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (addr & ~0xffff)
|
||||
return addressLookup(addr & 0xffff); // try pageless
|
||||
return NULL;
|
||||
}
|
||||
|
||||
#endif
|
196
extract/decrunch.c
Normal file
196
extract/decrunch.c
Normal file
@ -0,0 +1,196 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <memory.h>
|
||||
|
||||
static inline uint32_t fourcc(const char *p) {
|
||||
return (p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3];
|
||||
}
|
||||
|
||||
static inline uint16_t r16(uint8_t *p) {
|
||||
uint16_t r = *p++;
|
||||
r |= *p++ << 8;
|
||||
return r;
|
||||
}
|
||||
|
||||
static inline void w16(uint8_t *p, uint16_t v) {
|
||||
*p++ = v & 0xff;
|
||||
*p++ = (v >> 8) & 0xff;
|
||||
}
|
||||
|
||||
static inline uint32_t r24(uint8_t *p) {
|
||||
uint32_t r = *p++;
|
||||
r |= *p++ << 8;
|
||||
r |= *p++ << 16;
|
||||
return r;
|
||||
}
|
||||
|
||||
static inline uint32_t r32(uint8_t *p) {
|
||||
uint32_t r = *p++;
|
||||
r |= *p++ << 8;
|
||||
r |= *p++ << 16;
|
||||
r |= *p++ << 24;
|
||||
return r;
|
||||
}
|
||||
|
||||
static inline uint32_t r4(uint8_t *p) {
|
||||
uint32_t r = *p++ << 24;
|
||||
r |= *p++ << 16;
|
||||
r |= *p++ << 8;
|
||||
r |= *p++;
|
||||
return r;
|
||||
}
|
||||
|
||||
static void copyOps(uint8_t **to, uint8_t **from, uint16_t num) {
|
||||
uint8_t *dest = *to - num;
|
||||
uint8_t *src = *from - num;
|
||||
while (num > 0) {
|
||||
num--;
|
||||
dest[num] = src[num];
|
||||
}
|
||||
*to = dest;
|
||||
*from = src;
|
||||
}
|
||||
|
||||
static uint8_t adjustPage(uint64_t pos, uint16_t delta, uint8_t page) {
|
||||
uint64_t adjusted = (pos - delta) & ~0xff;
|
||||
uint64_t orig = pos & ~0xff;
|
||||
if (adjusted != orig)
|
||||
page += (orig - adjusted) >> 8;
|
||||
return page;
|
||||
}
|
||||
|
||||
static uint32_t decrunch(uint8_t **data) {
|
||||
uint8_t *ptr = *data;
|
||||
uint32_t opsOfs = r24(ptr);
|
||||
uint8_t *ops = ptr + opsOfs;
|
||||
|
||||
w16(ptr, r16(ops)); // overwrite offset to opcodes
|
||||
w16(ptr + 2, r16(ops + 2));
|
||||
uint16_t numCopy = ops[4];
|
||||
uint8_t totalPages = ops[5];
|
||||
uint32_t outlen = r16(ops + 6) * 256;
|
||||
*data = realloc(*data, outlen);
|
||||
ptr = *data + outlen;
|
||||
ops = *data + opsOfs;
|
||||
uint8_t page = 0;
|
||||
if (page < totalPages)
|
||||
page = adjustPage(ptr - *data, numCopy, page);
|
||||
if (page > totalPages)
|
||||
page = totalPages;
|
||||
copyOps(&ptr, &ops, numCopy);
|
||||
|
||||
do {
|
||||
uint8_t op = *--ops;
|
||||
if (op == 0) {
|
||||
numCopy = 0x100;
|
||||
} else {
|
||||
uint16_t moveOfs, numMove;
|
||||
if (op & 0x80) {
|
||||
if (op & 0x40) {
|
||||
numMove = op & 0x3f;
|
||||
if (numMove < 5) {
|
||||
numMove <<= 8;
|
||||
numMove |= *--ops;
|
||||
}
|
||||
numCopy = *--ops;
|
||||
} else {
|
||||
numMove = 4;
|
||||
numCopy = op & 0x3f;
|
||||
if (numCopy == 0x3f)
|
||||
numCopy = *--ops;
|
||||
}
|
||||
moveOfs = *--ops;
|
||||
if (moveOfs <= page) {
|
||||
moveOfs <<= 8;
|
||||
moveOfs |= *--ops;
|
||||
} else {
|
||||
moveOfs -= page;
|
||||
}
|
||||
} else {
|
||||
if (op & 0x40) {
|
||||
moveOfs = *--ops;
|
||||
numMove = 3;
|
||||
numCopy = op & 0x3f;
|
||||
} else {
|
||||
numCopy = 0;
|
||||
numMove = 2;
|
||||
moveOfs = op & 0x3f;
|
||||
}
|
||||
}
|
||||
uint8_t *from = ptr + moveOfs;
|
||||
if (page < totalPages)
|
||||
page = adjustPage(ptr - *data, numMove, page);
|
||||
if (page > totalPages)
|
||||
page = totalPages;
|
||||
copyOps(&ptr, &from, numMove);
|
||||
}
|
||||
if (numCopy) {
|
||||
if (page < totalPages)
|
||||
page = adjustPage(ptr - *data, numCopy, page);
|
||||
if (page > totalPages)
|
||||
page = totalPages;
|
||||
copyOps(&ptr, &ops, numCopy);
|
||||
}
|
||||
} while (ops > *data);
|
||||
|
||||
return outlen;
|
||||
}
|
||||
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc < 5) {
|
||||
fprintf(stderr, "Usage: %s <filename.2mg> block numblocks <outfile.dat> [raw]\n", argv[0]);
|
||||
fprintf(stderr," raw is optional, and just saves the block without decrunching\n");
|
||||
return -1;
|
||||
}
|
||||
FILE *f = fopen(argv[1], "rb");
|
||||
if (!f) {
|
||||
fprintf(stderr, "Couldn't open %s\n", argv[1]);
|
||||
return -1;
|
||||
}
|
||||
fseek(f, 0, SEEK_END);
|
||||
int len = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
uint8_t *data = malloc(len);
|
||||
fread(data, 1, len, f);
|
||||
fclose(f);
|
||||
uint8_t is2mg = 1;
|
||||
if (len < 0x16 || r4(data) != fourcc("2IMG") || r32(data + 0xc) != 1)
|
||||
is2mg = 0;
|
||||
|
||||
uint32_t numBlocks = is2mg ? r32(data + 0x14) : len / 512;
|
||||
uint32_t diskofs = is2mg ? r32(data + 0x18) : 0;
|
||||
|
||||
int block = atoi(argv[2]);
|
||||
if (block >= numBlocks) {
|
||||
fprintf(stderr, "Block too large\n");
|
||||
return -1;
|
||||
}
|
||||
int num = atoi(argv[3]);
|
||||
if (block + num > numBlocks) {
|
||||
fprintf(stderr, "Too many blocks\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint8_t *raw = data;
|
||||
if (is2mg) {
|
||||
raw = malloc(num * 512);
|
||||
memcpy(raw, data + diskofs + block * 512, num * 512);
|
||||
free(data);
|
||||
}
|
||||
|
||||
if (argc == 6 && strcmp(argv[5], "raw") == 0) {
|
||||
f = fopen(argv[4], "wb");
|
||||
fwrite(raw, 512, num, f);
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t outlen = decrunch(&raw);
|
||||
|
||||
f = fopen(argv[4], "wb");
|
||||
fwrite(raw, 1, outlen, f);
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
340
extract/disasm.c
Normal file
340
extract/disasm.c
Normal file
@ -0,0 +1,340 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <memory.h>
|
||||
#include "65816.h"
|
||||
#include "addresses.h"
|
||||
#include "tools.h"
|
||||
#include "prodos8.h"
|
||||
#include "prodos16.h"
|
||||
#include "smartport.h"
|
||||
|
||||
static inline uint32_t fourcc(const char *p) {
|
||||
return (p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3];
|
||||
}
|
||||
|
||||
static inline uint16_t r16(uint8_t *p) {
|
||||
uint16_t r = *p++;
|
||||
r |= *p++ << 8;
|
||||
return r;
|
||||
}
|
||||
|
||||
static inline uint32_t r24(uint8_t *p) {
|
||||
uint32_t r = *p++;
|
||||
r |= *p++ << 8;
|
||||
r |= *p++ << 16;
|
||||
return r;
|
||||
}
|
||||
|
||||
static inline uint32_t r32(uint8_t *p) {
|
||||
uint32_t r = *p++;
|
||||
r |= *p++ << 8;
|
||||
r |= *p++ << 16;
|
||||
r |= *p++ << 24;
|
||||
return r;
|
||||
}
|
||||
|
||||
static inline uint32_t r4(uint8_t *p) {
|
||||
uint32_t r = *p++ << 24;
|
||||
r |= *p++ << 16;
|
||||
r |= *p++ << 8;
|
||||
r |= *p++;
|
||||
return r;
|
||||
}
|
||||
|
||||
static void disasm(uint8_t *ptr, uint32_t addr, int len) {
|
||||
uint8_t *end = ptr + len;
|
||||
|
||||
uint8_t emu = 0;
|
||||
uint8_t flags = 0x30;
|
||||
uint16_t x = 0;
|
||||
uint32_t val;
|
||||
int8_t delta;
|
||||
int16_t delta16;
|
||||
uint32_t d6;
|
||||
uint8_t smart = 0, dos8 = 0, dos16 = 0;
|
||||
|
||||
while (ptr < end) {
|
||||
printf("%02x/%04x:", addr >> 16, addr & 0xffff);
|
||||
uint8_t *start = ptr;
|
||||
uint8_t opcode = *ptr++;
|
||||
|
||||
const char *inst = opcodes[opcode].inst;
|
||||
Address mode = opcodes[opcode].address;
|
||||
|
||||
if (smart == 1 || dos8 == 1) {
|
||||
mode = DB;
|
||||
inst = "db";
|
||||
}
|
||||
if (smart == 2 || dos8 == 2 || dos16 == 1) {
|
||||
mode = DW;
|
||||
inst = "dw";
|
||||
smart = 0;
|
||||
dos8 = 0;
|
||||
}
|
||||
if (smart == 3 || dos16 == 2) {
|
||||
mode = DD;
|
||||
inst = "dd";
|
||||
smart = 0;
|
||||
dos16 = 0;
|
||||
}
|
||||
|
||||
|
||||
if (start + addressSizes[mode] > end) {
|
||||
inst = "db";
|
||||
mode = DB;
|
||||
}
|
||||
uint16_t width = addressSizes[mode];
|
||||
if (mode == IMMM && (emu || (flags & 0x20)))
|
||||
width--;
|
||||
if (mode == IMMX && (emu || (flags & 0x10)))
|
||||
width--;
|
||||
addr += width;
|
||||
|
||||
for (int i = 0; i < width; i++) {
|
||||
printf(" %02x", start[i]);
|
||||
}
|
||||
for (int i = 0; i < 4 - width; i++) {
|
||||
printf(" ");
|
||||
}
|
||||
|
||||
printf(" %s", inst);
|
||||
for (int i = strlen(inst); i < 8; i++)
|
||||
printf(" ");
|
||||
|
||||
const char *comments = NULL;
|
||||
|
||||
switch (mode) {
|
||||
case IMP:
|
||||
break;
|
||||
case IMM:
|
||||
val = *ptr++;
|
||||
printf("#$%02x", val);
|
||||
if (opcode == 0xe2)
|
||||
flags |= val;
|
||||
else if (opcode == 0xc2)
|
||||
flags &= ~val;
|
||||
break;
|
||||
case IMMM:
|
||||
if ((flags & 0x20) || emu)
|
||||
printf("#$%02x", *ptr++);
|
||||
else {
|
||||
printf("#$%04x", r16(ptr)); ptr += 2;
|
||||
}
|
||||
break;
|
||||
case IMMX:
|
||||
if ((flags & 0x10) || emu) {
|
||||
x = *ptr++;
|
||||
printf("#$%02x", x);
|
||||
} else {
|
||||
x = r16(ptr); ptr += 2;
|
||||
printf("#$%04x", x);
|
||||
}
|
||||
break;
|
||||
case IMMS:
|
||||
printf("#$%04x", r16(ptr)); ptr += 2;
|
||||
break;
|
||||
case ABSO:
|
||||
val = r16(ptr); ptr += 2;
|
||||
printf("$%04x", val);
|
||||
comments = addressLookup(val);
|
||||
if (comments)
|
||||
printf(" ; %s", comments);
|
||||
break;
|
||||
case ABL:
|
||||
val = r24(ptr); ptr += 3;
|
||||
printf("$%02x/%04x", val >> 16, val & 0xffff);
|
||||
comments = addressLookup(val);
|
||||
if (comments)
|
||||
printf(" ; %s", comments);
|
||||
break;
|
||||
case ABX:
|
||||
printf("$%04x, x", r16(ptr)); ptr += 2;
|
||||
break;
|
||||
case ABY:
|
||||
printf("$%04x, y", r16(ptr)); ptr += 2;
|
||||
break;
|
||||
case ABLX:
|
||||
val = r24(ptr); ptr += 3;
|
||||
printf("$%02x/%04x, x", val >> 16, val & 0xffff);
|
||||
break;
|
||||
case AIX:
|
||||
printf("($%04x, x)", r16(ptr)); ptr += 2;
|
||||
break;
|
||||
case ZP:
|
||||
printf("$%02x", *ptr++);
|
||||
break;
|
||||
case ZPX:
|
||||
printf("$%02x, x", *ptr++);
|
||||
break;
|
||||
case ZPY:
|
||||
printf("$%02x, y", *ptr++);
|
||||
break;
|
||||
case ZPS:
|
||||
printf("$%02x, s", *ptr++);
|
||||
break;
|
||||
case IND:
|
||||
printf("($%04x)", r16(ptr)); ptr += 2;
|
||||
break;
|
||||
case INZ:
|
||||
printf("($%02x)", *ptr++);
|
||||
break;
|
||||
case INL:
|
||||
printf("[$%02x]", *ptr++);
|
||||
break;
|
||||
case INX:
|
||||
printf("($%02x, x)", *ptr++);
|
||||
break;
|
||||
case INY:
|
||||
printf("($%02x), y", *ptr++);
|
||||
break;
|
||||
case INLY:
|
||||
printf("[$%02x], y", *ptr++);
|
||||
break;
|
||||
case INS:
|
||||
printf("($%02x, s), y", *ptr++);
|
||||
break;
|
||||
case REL:
|
||||
delta = *ptr++;
|
||||
d6 = delta + addr;
|
||||
printf("$%04x", d6 & 0xffff);
|
||||
break;
|
||||
case RELL:
|
||||
delta16 = r16(ptr); ptr += 2;
|
||||
d6 = delta16 + addr;
|
||||
printf("$%02x/%04x", d6 >> 16, d6 & 0xffff);
|
||||
break;
|
||||
case BANK:
|
||||
val = *ptr++;
|
||||
printf("$%02x, $%02x", *ptr++, val);
|
||||
break;
|
||||
case DB:
|
||||
printf("$%02x", opcode);
|
||||
break;
|
||||
case DW:
|
||||
val = opcode | (*ptr++ << 8);
|
||||
printf("$%04x", val);
|
||||
break;
|
||||
case DD:
|
||||
printf("%08x", opcode | r24(ptr) << 8); ptr += 3;
|
||||
break;
|
||||
}
|
||||
|
||||
if (smart == 1) {
|
||||
comments = smartportLookup(opcode);
|
||||
if (comments)
|
||||
printf(" ; %s", comments);
|
||||
if (opcode >= 0x40) {
|
||||
smart++;
|
||||
}
|
||||
smart++;
|
||||
}
|
||||
|
||||
if (dos8 == 1) {
|
||||
comments = prodos8Lookup(opcode);
|
||||
if (comments)
|
||||
printf(" ; %s", comments);
|
||||
dos8++;
|
||||
}
|
||||
|
||||
if (dos16 == 1) {
|
||||
comments = prodos16Lookup(val);
|
||||
if (comments)
|
||||
printf(" ; %s", comments);
|
||||
dos16++;
|
||||
}
|
||||
|
||||
if (opcode == 0x18 && ptr[0] == 0xfb) { // clc xce
|
||||
emu = 0;
|
||||
printf(" ; 16bit mode");
|
||||
}
|
||||
if (opcode == 0x38 && ptr[0] == 0xfb) { // sec xce
|
||||
emu = 1;
|
||||
printf(" ; 8bit mode");
|
||||
}
|
||||
|
||||
if (opcode == 0xa2) { // ldx
|
||||
if (ptr[0] == 0x22) { // jsl
|
||||
if (r24(ptr + 1) == 0xe10000) { // jsl e1:0000
|
||||
comments = toolLookup(x);
|
||||
if (comments)
|
||||
printf(" ; %s", comments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (opcode == 0x20) { // JSR
|
||||
if (val == 0xc50d || val == 0xc70d)
|
||||
smart = 1;
|
||||
if (val == 0xbf00)
|
||||
dos8 = 1;
|
||||
}
|
||||
|
||||
if (opcode == 0x22) { // JSL
|
||||
if (val == 0xe100a8)
|
||||
dos16 = 1;
|
||||
}
|
||||
|
||||
printf("\n");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc < 3) {
|
||||
fprintf(stderr, "Usage: %s <file> <startaddr> [block] [num]\n", argv[0]);
|
||||
fprintf(stderr, " startaddr should be in hex\n");
|
||||
fprintf(stderr, " block is optional, 0 = default\n");
|
||||
fprintf(stderr, " num is number of blocks, optional\n");
|
||||
fprintf(stderr," If file isn't a 2mg, block and num are bytes\n");
|
||||
return -1;
|
||||
}
|
||||
FILE *f = fopen(argv[1], "rb");
|
||||
if (!f) {
|
||||
fprintf(stderr, "Couldn't open %s\n", argv[1]);
|
||||
return -1;
|
||||
}
|
||||
|
||||
fseek(f, 0, SEEK_END);
|
||||
int len = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
uint8_t *data = malloc(len);
|
||||
fread(data, 1, len, f);
|
||||
fclose(f);
|
||||
|
||||
uint32_t addr = strtol(argv[2], NULL, 16);
|
||||
|
||||
int block = 0;
|
||||
if (argc > 3)
|
||||
block = strtol(argv[3], NULL, 10);
|
||||
|
||||
int num = 0;
|
||||
if (argc > 4)
|
||||
num = strtol(argv[4], NULL, 10);
|
||||
|
||||
// if it's a 2img, find appropriate block
|
||||
if (len > 0x16 && r4(data) == fourcc("2IMG") && r32(data + 0xc) == 1) {
|
||||
uint32_t numBlocks = r32(data + 0x14);
|
||||
uint32_t diskofs = r32(data + 0x18);
|
||||
if (block >= numBlocks) {
|
||||
fprintf(stderr, "Block too large\n");
|
||||
return -1;
|
||||
}
|
||||
if (num == 0)
|
||||
num = numBlocks - block;
|
||||
if (block + num > numBlocks) {
|
||||
fprintf(stderr, "Too many blocks\n");
|
||||
return -1;
|
||||
}
|
||||
disasm(data + diskofs + block * 512, addr, num * 512);
|
||||
} else {
|
||||
// not a 2img, just a raw file..
|
||||
if (num == 0)
|
||||
num = len - block;
|
||||
if (block + num > len) {
|
||||
fprintf(stderr, "num is too long\n");
|
||||
return -1;
|
||||
}
|
||||
disasm(data + block, addr, num);
|
||||
}
|
||||
}
|
55
extract/dumptbl.c
Normal file
55
extract/dumptbl.c
Normal file
@ -0,0 +1,55 @@
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <memory.h>
|
||||
|
||||
static inline uint16_t r16(uint8_t *p) {
|
||||
uint16_t r = *p++;
|
||||
r |= *p++ << 8;
|
||||
return r;
|
||||
}
|
||||
|
||||
static inline uint32_t r32(uint8_t *p) {
|
||||
uint32_t r = *p++;
|
||||
r |= *p++ << 8;
|
||||
r |= *p++ << 16;
|
||||
r |= *p++ << 24;
|
||||
return r;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 3) {
|
||||
fprintf(stderr, "Usage: %s <file> <address>\n", argv[0]);
|
||||
return -1;
|
||||
}
|
||||
FILE *f = fopen(argv[1], "rb");
|
||||
if (!f) {
|
||||
fprintf(stderr, "Couldn't open %s\n", argv[1]);
|
||||
return -1;
|
||||
}
|
||||
fseek(f, 0, SEEK_END);
|
||||
int len = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
uint8_t *data = malloc(len);
|
||||
fread(data, 1, len, f);
|
||||
fclose(f);
|
||||
|
||||
uint32_t addr = strtol(argv[2], NULL, 16);
|
||||
if (addr > len - 4) {
|
||||
fprintf(stderr, "Address is too large\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint8_t done = 0;
|
||||
uint16_t start = r16(data + addr);
|
||||
addr += 2;
|
||||
do {
|
||||
uint32_t to = r32(data + addr); addr += 4;
|
||||
uint32_t pages = r32(data + addr); addr += 4;
|
||||
if (pages)
|
||||
printf("Address: $%02x:%04x Block #%d blocks: %d\n", to >> 16, to & 0xffff, start, pages / 2);
|
||||
start += pages / 2;
|
||||
if (pages == 0)
|
||||
done = 1;
|
||||
} while (!done);
|
||||
}
|
142
extract/prodos16.h
Normal file
142
extract/prodos16.h
Normal file
@ -0,0 +1,142 @@
|
||||
#ifndef __PRODOS16_H__
|
||||
#define __PRODOS16_H__
|
||||
|
||||
typedef struct {
|
||||
uint16_t call;
|
||||
const char *name;
|
||||
} Prodos16;
|
||||
|
||||
static Prodos16 prodos16[] = {
|
||||
{0x0001, "CREATE"},
|
||||
{0x0002, "DESTROY"},
|
||||
{0x0004, "CHANGE_PATH"},
|
||||
{0x0005, "SET_FILE_INFO"},
|
||||
{0x0006, "GET_FILE_INFO"},
|
||||
{0x0008, "VOLUME"},
|
||||
{0x0009, "SET_PREFIX"},
|
||||
{0x000a, "GET_PREFIX"},
|
||||
{0x000b, "CLEAR_BACKUP_BIT"},
|
||||
{0x0010, "OPEN"},
|
||||
{0x0011, "NEWLINE"},
|
||||
{0x0012, "READ"},
|
||||
{0x0013, "WRITE"},
|
||||
{0x0014, "CLOSE"},
|
||||
{0x0015, "FLUSH"},
|
||||
{0x0016, "SET_MARK"},
|
||||
{0x0017, "GET_MARK"},
|
||||
{0x0018, "SET_EOF"},
|
||||
{0x0019, "GET_EOF"},
|
||||
{0x001a, "SET_LEVEL"},
|
||||
{0x001b, "GET_LEVEL"},
|
||||
{0x001c, "GET_DIR_ENTRY"},
|
||||
{0x0020, "GET_DEV_NUM"},
|
||||
{0x0021, "GET_LAST_DEV"},
|
||||
{0x0022, "READ_BLOCK"},
|
||||
{0x0023, "WRITE_BLOCK"},
|
||||
{0x0024, "FORMAT"},
|
||||
{0x0025, "ERASE_DISK"},
|
||||
{0x0027, "GET_NAME"},
|
||||
{0x0028, "GET_BOOT_VOL"},
|
||||
{0x0029, "QUIT"},
|
||||
{0x002a, "GET_VERSION"},
|
||||
{0x002c, "D_INFO"},
|
||||
{0x0031, "ALLOC_INTERRUPT"},
|
||||
{0x0032, "DEALLOCATE_INTERRUPT"},
|
||||
{0x0101, "Get_LInfo"},
|
||||
{0x0102, "Set_LInfo"},
|
||||
{0x0103, "Get_Lang"},
|
||||
{0x0104, "Set_Lang"},
|
||||
{0x0105, "Error"},
|
||||
{0x0106, "Set_Variable"},
|
||||
{0x0107, "Version"},
|
||||
{0x0108, "Read_Indexed"},
|
||||
{0x0109, "Init_Wildcard"},
|
||||
{0x010a, "Next_Wildcard"},
|
||||
{0x010b, "Read_Variable"},
|
||||
{0x010c, "ChangeVector"},
|
||||
{0x010d, "Execute"},
|
||||
{0x010e, "FastFile"},
|
||||
{0x010f, "Direction"},
|
||||
{0x0110, "Redirect"},
|
||||
{0x0113, "Stop"},
|
||||
{0x0114, "ExpandDevices"},
|
||||
{0x0115, "UnsetVariable"},
|
||||
{0x0116, "Export"},
|
||||
{0x0117, "PopVariables"},
|
||||
{0x0118, "PushVariables"},
|
||||
{0x0119, "SetStopFlag"},
|
||||
{0x011a, "ConsoleOut"},
|
||||
{0x011b, "SetIODevices"},
|
||||
{0x011c, "GetIODevices"},
|
||||
{0x011d, "GetCommand"},
|
||||
{0x2001, "Create"},
|
||||
{0x2002, "Destroy"},
|
||||
{0x2003, "OSShutdown"},
|
||||
{0x2004, "ChangePath"},
|
||||
{0x2005, "SetFileInfo"},
|
||||
{0x2006, "GetFileInfo"},
|
||||
{0x2007, "JudgeName"},
|
||||
{0x2008, "Volume"},
|
||||
{0x2009, "SetPrefix"},
|
||||
{0x200a, "GetPrefix"},
|
||||
{0x200b, "ClearBackup"},
|
||||
{0x200c, "SetSysPrefs"},
|
||||
{0x200d, "Null"},
|
||||
{0x200e, "ExpandPath"},
|
||||
{0x200f, "GetSysPrefs"},
|
||||
{0x2010, "Open"},
|
||||
{0x2011, "NewLine"},
|
||||
{0x2012, "Read"},
|
||||
{0x2013, "Write"},
|
||||
{0x2014, "Close"},
|
||||
{0x2015, "Flush"},
|
||||
{0x2016, "SetMark"},
|
||||
{0x2017, "GetMark"},
|
||||
{0x2018, "SetEOF"},
|
||||
{0x2019, "GetEOF"},
|
||||
{0x201a, "SetLevel"},
|
||||
{0x201b, "GetLevel"},
|
||||
{0x201c, "GetDirEntry"},
|
||||
{0x201d, "BeginSession"},
|
||||
{0x201e, "EndSession"},
|
||||
{0x201f, "SessionStatus"},
|
||||
{0x2020, "GetDevNumber"},
|
||||
{0x2024, "Format"},
|
||||
{0x2025, "EraseDisk"},
|
||||
{0x2026, "ResetCache"},
|
||||
{0x2027, "GetName"},
|
||||
{0x2028, "GetBoolVol"},
|
||||
{0x2029, "Quit"},
|
||||
{0x202a, "GetVersion"},
|
||||
{0x202b, "GetFSTInfo"},
|
||||
{0x202c, "DInfo"},
|
||||
{0x202d, "DStatus"},
|
||||
{0x202e, "DControl"},
|
||||
{0x202f, "DRead"},
|
||||
{0x2030, "DWrite"},
|
||||
{0x2031, "BindInt"},
|
||||
{0x2032, "UnbindInt"},
|
||||
{0x2033, "FSTSpecific"},
|
||||
{0x2034, "AddNotifyProc"},
|
||||
{0x2035, "DelNotifyProc"},
|
||||
{0x2036, "DRename"},
|
||||
{0x2037, "GetStdRefNum"},
|
||||
{0x2038, "GetRefNum"},
|
||||
{0x2039, "GetRefInfo"},
|
||||
{0x203a, "SetStdRefNum"}
|
||||
};
|
||||
|
||||
#define numProdos16 (sizeof(prodos16) / sizeof(prodos16[0]))
|
||||
|
||||
static const char *prodos16Lookup(uint16_t call) {
|
||||
for (int i = 0; i < numProdos16; i++) {
|
||||
if (prodos16[i].call >= call) {
|
||||
if (prodos16[i].call == call)
|
||||
return prodos16[i].name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
#endif
|
54
extract/prodos8.h
Normal file
54
extract/prodos8.h
Normal file
@ -0,0 +1,54 @@
|
||||
#ifndef __PRODOS8_H__
|
||||
#define __PRODOS8_H__
|
||||
|
||||
typedef struct {
|
||||
uint16_t call;
|
||||
const char *name;
|
||||
} Prodos8;
|
||||
|
||||
static Prodos8 prodos8[] = {
|
||||
{0x0040, "ALLOC_INTERRUPT"},
|
||||
{0x0041, "DEALLOC_INTERRUPT"},
|
||||
{0x0042, "AppleTalk"},
|
||||
{0x0043, "SpecialOpenFork"},
|
||||
{0x0044, "ByteRangeLock"},
|
||||
{0x0065, "QUIT"},
|
||||
{0x0080, "READ_BLOCK"},
|
||||
{0x0081, "WRITE_BLOCK"},
|
||||
{0x0082, "GET_TIME"},
|
||||
{0x00c0, "CREATE"},
|
||||
{0x00c1, "DESTROY"},
|
||||
{0x00c2, "RENAME"},
|
||||
{0x00c3, "SetFileInfo"},
|
||||
{0x00c4, "GetFileInfo"},
|
||||
{0x00c5, "ONLINE"},
|
||||
{0x00c6, "SET_PREFIX"},
|
||||
{0x00c7, "GET_PREFIX"},
|
||||
{0x00c8, "OPEN"},
|
||||
{0x00c9, "NEWLINE"},
|
||||
{0x00ca, "READ"},
|
||||
{0x00cb, "WRITE"},
|
||||
{0x00cc, "CLOSE"},
|
||||
{0x00cd, "FLUSH"},
|
||||
{0x00ce, "SET_MARK"},
|
||||
{0x00cf, "GET_MARK"},
|
||||
{0x00d0, "SET_EOF"},
|
||||
{0x00d1, "GET_EOF"},
|
||||
{0x00d2, "SET_BUF"},
|
||||
{0x00d3, "GET_BUF"}
|
||||
};
|
||||
|
||||
#define numProdos8 (sizeof(prodos8) / sizeof(prodos8[0]))
|
||||
|
||||
static const char *prodos8Lookup(uint16_t call) {
|
||||
for (int i = 0; i < numProdos8; i++) {
|
||||
if (prodos8[i].call >= call) {
|
||||
if (prodos8[i].call == call)
|
||||
return prodos8[i].name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
#endif
|
45
extract/smartport.h
Normal file
45
extract/smartport.h
Normal file
@ -0,0 +1,45 @@
|
||||
#ifndef __SMARTPORT_H_
|
||||
#define __SMARTPORT_H_
|
||||
|
||||
typedef struct {
|
||||
uint8_t call;
|
||||
const char *name;
|
||||
} SmartPort;
|
||||
|
||||
static SmartPort smartport[] = {
|
||||
{0x00, "Status"},
|
||||
{0x01, "Read"},
|
||||
{0x02, "Write"},
|
||||
{0x03, "Format"},
|
||||
{0x04, "Control"},
|
||||
{0x05, "Init"},
|
||||
{0x06, "Open"},
|
||||
{0x07, "Close"},
|
||||
{0x08, "Read"},
|
||||
{0x09, "Write"},
|
||||
{0x40, "Status"},
|
||||
{0x41, "Read"},
|
||||
{0x42, "Write"},
|
||||
{0x43, "Format"},
|
||||
{0x44, "Control"},
|
||||
{0x45, "Init"},
|
||||
{0x46, "Open"},
|
||||
{0x47, "Close"},
|
||||
{0x48, "Read"},
|
||||
{0x49, "Write"}
|
||||
};
|
||||
|
||||
#define numSmartPort (sizeof(smartport) / sizeof(smartport[0]))
|
||||
|
||||
static const char *smartportLookup(uint8_t call) {
|
||||
for (int i = 0; i < numSmartPort; i++) {
|
||||
if (smartport[i].call >= call) {
|
||||
if (smartport[i].call == call)
|
||||
return smartport[i].name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
#endif
|
1288
extract/tools.h
Normal file
1288
extract/tools.h
Normal file
File diff suppressed because it is too large
Load Diff
47
extract/trimmusic.c
Normal file
47
extract/trimmusic.c
Normal file
@ -0,0 +1,47 @@
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <memory.h>
|
||||
|
||||
static inline uint16_t r16(uint8_t *p) {
|
||||
uint16_t r = *p++;
|
||||
r |= *p++ << 8;
|
||||
return r;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 4) {
|
||||
fprintf(stderr, "Usage: %s <file> <start> <outfile>\n", argv[0]);
|
||||
fprintf(stderr, " start is in hex\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
FILE *f = fopen(argv[1], "rb");
|
||||
if (!f) {
|
||||
fprintf(stderr, "Couldn't open %s\n", argv[1]);
|
||||
return -1;
|
||||
}
|
||||
fseek(f, 0, SEEK_END);
|
||||
int len = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
uint8_t *data = malloc(len);
|
||||
fread(data, 1, len, f);
|
||||
fclose(f);
|
||||
|
||||
uint32_t start = strtol(argv[2], NULL, 16);
|
||||
if (start > len - 4) {
|
||||
fprintf(stderr, "Start address is too large\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint32_t blockLen = r16(data + start + 6);
|
||||
uint32_t fileLen = 600 + blockLen * 3 + 30;
|
||||
|
||||
f = fopen(argv[3], "wb");
|
||||
if (!f) {
|
||||
fprintf(stderr, "Couldn't create %s\n", argv[3]);
|
||||
return -1;
|
||||
}
|
||||
fwrite(data + start, 1, fileLen, f);
|
||||
fclose(f);
|
||||
}
|
47
extract/trimwb.c
Normal file
47
extract/trimwb.c
Normal file
@ -0,0 +1,47 @@
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <memory.h>
|
||||
|
||||
static inline uint16_t r16(uint8_t *p) {
|
||||
uint16_t r = *p++;
|
||||
r |= *p++ << 8;
|
||||
return r;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 4) {
|
||||
fprintf(stderr, "Usage: %s <file> <start> <outfile>\n", argv[0]);
|
||||
fprintf(stderr, " start is in hex\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
FILE *f = fopen(argv[1], "rb");
|
||||
if (!f) {
|
||||
fprintf(stderr, "Couldn't open %s\n", argv[1]);
|
||||
return -1;
|
||||
}
|
||||
fseek(f, 0, SEEK_END);
|
||||
int len = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
uint8_t *data = malloc(len);
|
||||
fread(data, 1, len, f);
|
||||
fclose(f);
|
||||
|
||||
uint32_t start = strtol(argv[2], NULL, 16);
|
||||
if (start > len - 4) {
|
||||
fprintf(stderr, "Start address is too large\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint16_t numInst = r16(data + start) & 0xff;
|
||||
uint32_t fileLen = numInst * 0x5c + 0x1005e + 32;
|
||||
|
||||
f = fopen(argv[3], "wb");
|
||||
if (!f) {
|
||||
fprintf(stderr, "Couldn't create %s\n", argv[3]);
|
||||
return -1;
|
||||
}
|
||||
fwrite(data + start, 1, fileLen, f);
|
||||
fclose(f);
|
||||
}
|
14
fta.html
Normal file
14
fta.html
Normal file
@ -0,0 +1,14 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>FTA Player</title>
|
||||
<script type="text/javascript" src="fta.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="main.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<h2>FTA Player</h2>
|
||||
<div>Currently playing: <span id="loaded">-none-</span></div>
|
||||
<div id="controls"></div>
|
||||
<h3>Available songs</h3>
|
||||
<div id="songlist"></div>
|
||||
</body>
|
||||
</html>
|
529
fta.js
Normal file
529
fta.js
Normal file
@ -0,0 +1,529 @@
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (_) try {
|
||||
if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [0, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
var _this = this;
|
||||
var Song = (function () {
|
||||
function Song() {
|
||||
}
|
||||
return Song;
|
||||
}());
|
||||
var FTA = (function () {
|
||||
function FTA() {
|
||||
this.player = null;
|
||||
}
|
||||
FTA.prototype.getSongList = function (path) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
return __generator(this, function (_a) {
|
||||
return [2, new Promise(function (resolve) {
|
||||
var req = new XMLHttpRequest();
|
||||
req.open('GET', path, true);
|
||||
req.onload = function () {
|
||||
resolve(JSON.parse(req.responseText));
|
||||
};
|
||||
req.send(null);
|
||||
})];
|
||||
});
|
||||
});
|
||||
};
|
||||
FTA.prototype.open = function (name, music, wb, inst, delta) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var _this = this;
|
||||
var loaded, song, _a, wavebank, _b, instdef, _c, controls, stop_1, play;
|
||||
return __generator(this, function (_d) {
|
||||
switch (_d.label) {
|
||||
case 0:
|
||||
this.name = name;
|
||||
loaded = document.getElementById('loaded');
|
||||
if (loaded) {
|
||||
loaded.textContent = 'loading...';
|
||||
}
|
||||
_a = Handle.bind;
|
||||
return [4, this.load(music)];
|
||||
case 1:
|
||||
song = new (_a.apply(Handle, [void 0, _d.sent()]))();
|
||||
_b = Handle.bind;
|
||||
return [4, this.load(wb)];
|
||||
case 2:
|
||||
wavebank = new (_b.apply(Handle, [void 0, _d.sent()]))();
|
||||
_c = Handle.bind;
|
||||
return [4, this.load(inst)];
|
||||
case 3:
|
||||
instdef = new (_c.apply(Handle, [void 0, _d.sent()]))();
|
||||
if (this.player) {
|
||||
this.player.stop();
|
||||
}
|
||||
this.player = new FTAPlayer(song, wavebank, instdef, delta);
|
||||
if (loaded) {
|
||||
loaded.textContent = name;
|
||||
}
|
||||
controls = document.getElementById('controls');
|
||||
if (controls) {
|
||||
while (controls.firstChild) {
|
||||
controls.removeChild(controls.firstChild);
|
||||
}
|
||||
stop_1 = document.createElement('button');
|
||||
stop_1.textContent = '\u23f9';
|
||||
stop_1.addEventListener('click', function () {
|
||||
if (_this.player) {
|
||||
_this.player.stop();
|
||||
}
|
||||
});
|
||||
controls.appendChild(stop_1);
|
||||
play = document.createElement('button');
|
||||
play.textContent = '\u25b6';
|
||||
play.addEventListener('click', function () {
|
||||
if (_this.player) {
|
||||
_this.player.play();
|
||||
}
|
||||
});
|
||||
controls.appendChild(play);
|
||||
}
|
||||
return [2];
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
FTA.prototype.load = function (file) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
return __generator(this, function (_a) {
|
||||
return [2, new Promise(function (resolve) {
|
||||
var req = new XMLHttpRequest();
|
||||
req.open('GET', file, true);
|
||||
req.responseType = 'arraybuffer';
|
||||
req.onload = function () {
|
||||
if (req.response) {
|
||||
resolve(new Uint8Array(req.response));
|
||||
}
|
||||
};
|
||||
req.send(null);
|
||||
})];
|
||||
});
|
||||
});
|
||||
};
|
||||
return FTA;
|
||||
}());
|
||||
document.addEventListener('DOMContentLoaded', function () { return __awaiter(_this, void 0, void 0, function () {
|
||||
var _this = this;
|
||||
var fta, songs, list, _i, songs_1, song, row;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
fta = new FTA();
|
||||
return [4, fta.getSongList('ftasongs.json')];
|
||||
case 1:
|
||||
songs = _a.sent();
|
||||
list = document.getElementById('songlist');
|
||||
if (!list) {
|
||||
return [2];
|
||||
}
|
||||
for (_i = 0, songs_1 = songs; _i < songs_1.length; _i++) {
|
||||
song = songs_1[_i];
|
||||
row = document.createElement('div');
|
||||
row.dataset.name = song.name;
|
||||
row.dataset.music = song.music;
|
||||
row.dataset.wb = song.wb;
|
||||
row.dataset.inst = song.inst;
|
||||
row.dataset.delta = song.delta.toString(10);
|
||||
row.appendChild(document.createTextNode(song.name));
|
||||
row.addEventListener('click', function (event) { return __awaiter(_this, void 0, void 0, function () {
|
||||
var target;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
target = event.target;
|
||||
return [4, fta.open(target.dataset.name, target.dataset.music, target.dataset.wb, target.dataset.inst, parseInt(target.dataset.delta, 10))];
|
||||
case 1:
|
||||
_a.sent();
|
||||
return [2];
|
||||
}
|
||||
});
|
||||
}); });
|
||||
list.appendChild(row);
|
||||
}
|
||||
return [2];
|
||||
}
|
||||
});
|
||||
}); });
|
||||
var Channel = (function () {
|
||||
function Channel() {
|
||||
this.ticks = 1;
|
||||
this.offset = 0;
|
||||
this.pos = 0;
|
||||
this.notes = [];
|
||||
this.osc = 0;
|
||||
this.pointer = 0;
|
||||
this.size = 0;
|
||||
this.volume = 0;
|
||||
this.panning = 0;
|
||||
this.control = 0;
|
||||
this.freq = 0;
|
||||
}
|
||||
return Channel;
|
||||
}());
|
||||
var FTAPlayer = (function () {
|
||||
function FTAPlayer(music, wavebank, inst, delta) {
|
||||
var _this = this;
|
||||
this.stereo = true;
|
||||
this.ticksLeft = 0;
|
||||
this.channels = [];
|
||||
this.active = [];
|
||||
this.es5503 = new ES5503(function (osc) { _this.irq(osc); });
|
||||
wavebank.seek(0);
|
||||
this.es5503.setRam(wavebank.read(wavebank.length));
|
||||
this.es5503.setEnabled(0x3e);
|
||||
for (var i = 0; i < 32; i++) {
|
||||
this.channels.push(new Channel());
|
||||
}
|
||||
inst.seek(0);
|
||||
var base = inst.r16();
|
||||
while (base != 0xffff) {
|
||||
var ch = inst.r8();
|
||||
this.active.push(ch);
|
||||
this.channels[ch].osc = ch;
|
||||
this.channels[ch].pointer = inst.r8();
|
||||
this.channels[ch].size = inst.r8();
|
||||
this.channels[ch].volume = inst.r8();
|
||||
this.channels[ch].panning = inst.r8();
|
||||
this.channels[ch].control = inst.r8();
|
||||
music.seek(base - delta);
|
||||
var channelLen = music.r16();
|
||||
for (var i = 0; i < channelLen; i++) {
|
||||
var freq = music.r16();
|
||||
var time = music.r8();
|
||||
this.channels[ch].notes.push({ freq: freq, time: time });
|
||||
}
|
||||
base = inst.r16();
|
||||
}
|
||||
inst.seek(0x46);
|
||||
this.es5503.setFrequency(31, inst.r16());
|
||||
this.es5503.setControl(31, 8);
|
||||
}
|
||||
FTAPlayer.prototype.play = function () {
|
||||
var _this = this;
|
||||
try {
|
||||
this.ctx = new AudioContext();
|
||||
}
|
||||
catch (e) {
|
||||
alert('No audio support');
|
||||
return;
|
||||
}
|
||||
this.audioNode = this.ctx.createScriptProcessor(0, 0, 2);
|
||||
this.audioNode.onaudioprocess = function (evt) {
|
||||
_this.render(evt);
|
||||
};
|
||||
this.audioNode.connect(this.ctx.destination);
|
||||
};
|
||||
FTAPlayer.prototype.stop = function () {
|
||||
if (this.audioNode) {
|
||||
this.audioNode.disconnect();
|
||||
}
|
||||
this.audioNode = undefined;
|
||||
if (this.ctx) {
|
||||
this.ctx.close();
|
||||
}
|
||||
this.ctx = undefined;
|
||||
};
|
||||
FTAPlayer.prototype.render = function (evt) {
|
||||
var sampleRate = evt.outputBuffer.sampleRate;
|
||||
var leftBuf = evt.outputBuffer.getChannelData(0);
|
||||
var rightBuf = evt.outputBuffer.getChannelData(1);
|
||||
for (var i = 0; i < evt.outputBuffer.length; i++) {
|
||||
this.ticksLeft -= 26320;
|
||||
if (this.ticksLeft <= 0) {
|
||||
this.ticksLeft += sampleRate;
|
||||
this.es5503.tick();
|
||||
}
|
||||
var _a = this.es5503.render(), left = _a[0], right = _a[1];
|
||||
if (!this.stereo) {
|
||||
leftBuf[i] = (left + right) * 0.707;
|
||||
rightBuf[i] = leftBuf[i];
|
||||
}
|
||||
else {
|
||||
leftBuf[i] = left;
|
||||
rightBuf[i] = right;
|
||||
}
|
||||
}
|
||||
};
|
||||
FTAPlayer.prototype.irq = function (osc) {
|
||||
if (osc != 31) {
|
||||
var ch = this.channels[osc & 0xfc];
|
||||
this.noteOff(ch);
|
||||
this.noteOn(ch);
|
||||
}
|
||||
else {
|
||||
for (var _i = 0, _a = this.active; _i < _a.length; _i++) {
|
||||
var c = _a[_i];
|
||||
var ch = this.channels[c];
|
||||
ch.ticks--;
|
||||
if (ch.ticks == 0) {
|
||||
this.noteOff(ch);
|
||||
ch.ticks = ch.notes[ch.pos].time;
|
||||
ch.freq = ch.notes[ch.pos].freq;
|
||||
ch.pos++;
|
||||
if (ch.pos >= ch.notes.length) {
|
||||
ch.pos = 0;
|
||||
}
|
||||
this.noteOn(ch);
|
||||
ch.osc ^= 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
FTAPlayer.prototype.noteOn = function (ch) {
|
||||
this.es5503.setFrequency(ch.osc, ch.freq * 2);
|
||||
this.es5503.setFrequency(ch.osc + 1, ch.freq * 2);
|
||||
var vol = ch.volume & 0xf;
|
||||
this.es5503.setVolume(ch.osc, vol << 3);
|
||||
this.es5503.setVolume(ch.osc + 1, vol << 3);
|
||||
this.es5503.setVolume(ch.osc ^ 2, vol << 3);
|
||||
this.es5503.setVolume((ch.osc ^ 2) + 1, vol << 3);
|
||||
this.es5503.setPointer(ch.osc, ch.pointer);
|
||||
this.es5503.setPointer(ch.osc + 1, ch.pointer);
|
||||
this.es5503.setSize(ch.osc, ch.size);
|
||||
this.es5503.setSize(ch.osc + 1, ch.size);
|
||||
var a = 2;
|
||||
var b = 0x12;
|
||||
if (ch.panning == 0) {
|
||||
b = 2;
|
||||
}
|
||||
else if (ch.panning == 1) {
|
||||
a = 0x12;
|
||||
}
|
||||
this.es5503.setControl(ch.osc, a | ch.control);
|
||||
this.es5503.setControl(ch.osc + 1, b | ch.control);
|
||||
};
|
||||
FTAPlayer.prototype.noteOff = function (ch) {
|
||||
this.es5503.setControl(ch.osc, 7);
|
||||
this.es5503.setControl(ch.osc + 1, 7);
|
||||
};
|
||||
return FTAPlayer;
|
||||
}());
|
||||
var Mode;
|
||||
(function (Mode) {
|
||||
Mode[Mode["freeRun"] = 0] = "freeRun";
|
||||
Mode[Mode["oneShot"] = 1] = "oneShot";
|
||||
Mode[Mode["sync"] = 2] = "sync";
|
||||
Mode[Mode["swap"] = 3] = "swap";
|
||||
})(Mode || (Mode = {}));
|
||||
var Oscillator = (function () {
|
||||
function Oscillator() {
|
||||
this.pointer = 0;
|
||||
this.frequency = 0;
|
||||
this.size = 0;
|
||||
this.control = 1;
|
||||
this.volume = 0;
|
||||
this.data = 0;
|
||||
this.resolution = 0;
|
||||
this.accumulator = 0;
|
||||
this.ptr = 0;
|
||||
this.shift = 9;
|
||||
this.max = 0xff;
|
||||
}
|
||||
return Oscillator;
|
||||
}());
|
||||
var ES5503 = (function () {
|
||||
function ES5503(irq) {
|
||||
this.waveTable = new Float32Array(0x10000);
|
||||
this.oscillators = [];
|
||||
this.enabled = 0;
|
||||
this.waveSizes = [
|
||||
0x100, 0x200, 0x400, 0x800, 0x1000, 0x2000, 0x4000, 0x8000,
|
||||
];
|
||||
this.waveMasks = [
|
||||
0x1ff00, 0x1fe00, 0x1fc00, 0x1f800, 0x1f000, 0x1e000, 0x1c000, 0x18000,
|
||||
];
|
||||
this.irq = irq;
|
||||
for (var i = 0; i < 32; i++) {
|
||||
this.oscillators.push(new Oscillator());
|
||||
}
|
||||
}
|
||||
ES5503.prototype.setEnabled = function (enabled) {
|
||||
this.enabled = enabled >> 1;
|
||||
};
|
||||
ES5503.prototype.setRam = function (bank) {
|
||||
for (var i = 0; i < bank.length; i++) {
|
||||
this.waveTable[i] = (bank[i] - 128) / 128;
|
||||
}
|
||||
};
|
||||
ES5503.prototype.setFrequency = function (osc, freq) {
|
||||
this.oscillators[osc].frequency = freq;
|
||||
};
|
||||
ES5503.prototype.setVolume = function (osc, vol) {
|
||||
this.oscillators[osc].volume = vol / 127;
|
||||
};
|
||||
ES5503.prototype.setPointer = function (osc, ptr) {
|
||||
this.oscillators[osc].pointer = ptr << 8;
|
||||
this.recalc(osc);
|
||||
};
|
||||
ES5503.prototype.setSize = function (osc, size) {
|
||||
this.oscillators[osc].size = (size >> 3) & 7;
|
||||
this.oscillators[osc].resolution = size & 7;
|
||||
this.recalc(osc);
|
||||
};
|
||||
ES5503.prototype.setControl = function (osc, ctl) {
|
||||
var prev = this.oscillators[osc].control & 1;
|
||||
this.oscillators[osc].control = ctl;
|
||||
var mode = (ctl >> 1) & 3;
|
||||
if (!(ctl & 1) && prev) {
|
||||
if (mode == Mode.sync) {
|
||||
this.oscillators[osc ^ 1].control &= ~1;
|
||||
this.oscillators[osc ^ 1].accumulator = 0;
|
||||
}
|
||||
this.oscillators[osc].accumulator = 0;
|
||||
}
|
||||
};
|
||||
ES5503.prototype.stop = function (osc) {
|
||||
this.oscillators[osc].control &= 0xf7;
|
||||
this.oscillators[osc].control |= 1;
|
||||
this.oscillators[osc].accumulator = 0;
|
||||
};
|
||||
ES5503.prototype.go = function (osc) {
|
||||
this.oscillators[osc].control &= ~1;
|
||||
};
|
||||
ES5503.prototype.tick = function () {
|
||||
for (var osc = 0; osc <= this.enabled; osc++) {
|
||||
var cur = this.oscillators[osc];
|
||||
if (!(cur.control & 1)) {
|
||||
var base = cur.accumulator >> cur.shift;
|
||||
var ofs = (base & cur.max) + cur.ptr;
|
||||
cur.data = this.waveTable[ofs] * cur.volume;
|
||||
cur.accumulator += cur.frequency;
|
||||
if (this.waveTable[ofs] == -1) {
|
||||
this.halted(osc, true);
|
||||
}
|
||||
else if (base >= cur.max) {
|
||||
this.halted(osc, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
ES5503.prototype.render = function () {
|
||||
var left = 0;
|
||||
var right = 0;
|
||||
for (var osc = 0; osc <= this.enabled; osc++) {
|
||||
var cur = this.oscillators[osc];
|
||||
if (!(cur.control & 1)) {
|
||||
if (cur.control & 0x10) {
|
||||
right += cur.data;
|
||||
}
|
||||
else {
|
||||
left += cur.data;
|
||||
}
|
||||
}
|
||||
}
|
||||
var spread = (this.enabled - 2) / 4;
|
||||
return [left / spread, right / spread];
|
||||
};
|
||||
ES5503.prototype.recalc = function (osc) {
|
||||
var cur = this.oscillators[osc];
|
||||
cur.shift = (cur.resolution + 9) - cur.size;
|
||||
cur.ptr = cur.pointer & this.waveMasks[cur.size];
|
||||
cur.max = this.waveSizes[cur.size] - 1;
|
||||
};
|
||||
ES5503.prototype.halted = function (osc, interrupted) {
|
||||
var cur = this.oscillators[osc];
|
||||
var mode = (cur.control >> 1) & 3;
|
||||
if (interrupted || mode != Mode.freeRun) {
|
||||
cur.control |= 1;
|
||||
}
|
||||
else {
|
||||
var base = (cur.accumulator >> cur.shift) - cur.max;
|
||||
cur.accumulator = Math.max(base, 0) << cur.shift;
|
||||
}
|
||||
if (mode == Mode.swap) {
|
||||
var swap = this.oscillators[osc ^ 1];
|
||||
swap.control &= ~1;
|
||||
swap.accumulator = 0;
|
||||
}
|
||||
if (cur.control & 8) {
|
||||
this.irq(osc);
|
||||
}
|
||||
};
|
||||
return ES5503;
|
||||
}());
|
||||
var Handle = (function () {
|
||||
function Handle(data) {
|
||||
this.pos = 0;
|
||||
this.data = data;
|
||||
this.length = data.length;
|
||||
}
|
||||
Handle.prototype.eof = function () {
|
||||
return this.pos >= this.length;
|
||||
};
|
||||
Handle.prototype.r8 = function () {
|
||||
return this.data[this.pos++];
|
||||
};
|
||||
Handle.prototype.r16 = function () {
|
||||
var v = this.data[this.pos++];
|
||||
v |= this.data[this.pos++] << 8;
|
||||
return v;
|
||||
};
|
||||
Handle.prototype.r24 = function () {
|
||||
var v = this.data[this.pos++];
|
||||
v |= this.data[this.pos++] << 8;
|
||||
v |= this.data[this.pos++] << 16;
|
||||
return v;
|
||||
};
|
||||
Handle.prototype.r32 = function () {
|
||||
var v = this.data[this.pos++];
|
||||
v |= this.data[this.pos++] << 8;
|
||||
v |= this.data[this.pos++] << 16;
|
||||
v |= this.data[this.pos++] << 24;
|
||||
return v >>> 0;
|
||||
};
|
||||
Handle.prototype.r4 = function () {
|
||||
var r = '';
|
||||
for (var i = 0; i < 4; i++) {
|
||||
r += String.fromCharCode(this.data[this.pos++]);
|
||||
}
|
||||
return r;
|
||||
};
|
||||
Handle.prototype.seek = function (pos) {
|
||||
this.pos = pos;
|
||||
};
|
||||
Handle.prototype.skip = function (len) {
|
||||
this.pos += len;
|
||||
};
|
||||
Handle.prototype.tell = function () {
|
||||
return this.pos;
|
||||
};
|
||||
Handle.prototype.read = function (len) {
|
||||
var oldpos = this.pos;
|
||||
this.pos += len;
|
||||
return this.data.subarray(oldpos, this.pos);
|
||||
};
|
||||
return Handle;
|
||||
}());
|
||||
//# sourceMappingURL=fta.js.map
|
27
ftasongs.json
Normal file
27
ftasongs.json
Normal file
@ -0,0 +1,27 @@
|
||||
[
|
||||
{"name": "Nucleus - Intro",
|
||||
"music": "songs/nucleus/intro.song",
|
||||
"wb": "songs/nucleus/intro.wb",
|
||||
"inst": "songs/nucleus/intro.inst",
|
||||
"delta": 34304},
|
||||
{"name": "Nucleus - Main 1",
|
||||
"music": "songs/nucleus/main1.song",
|
||||
"wb": "songs/nucleus/main.wb",
|
||||
"inst": "songs/nucleus/main1.inst",
|
||||
"delta": 34304},
|
||||
{"name": "Nucleus - Main 2",
|
||||
"music": "songs/nucleus/main2.song",
|
||||
"wb": "songs/nucleus/main.wb",
|
||||
"inst": "songs/nucleus/main2.inst",
|
||||
"delta": 34304},
|
||||
{"name": "Nucleus - Main 3",
|
||||
"music": "songs/nucleus/main3.song",
|
||||
"wb": "songs/nucleus/main.wb",
|
||||
"inst": "songs/nucleus/main3.inst",
|
||||
"delta": 34304},
|
||||
{"name": "Photonix - About",
|
||||
"music": "songs/photonix/main.song",
|
||||
"wb": "songs/photonix/main.wb",
|
||||
"inst": "songs/photonix/main.inst",
|
||||
"delta": 20480}
|
||||
]
|
45
index.html
Normal file
45
index.html
Normal file
@ -0,0 +1,45 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Javascript Soundsmith Player</title>
|
||||
<script type="text/javascript" src="smith.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="main.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Javascript Soundsmith Player</h2>
|
||||
<p>
|
||||
This is a 100% javascript Soundsmith Player. Soundsmith was a music program
|
||||
released in the late 80s for the Apple IIgs. Many games and demos used
|
||||
Soundsmith for their music. I've included some examples with the player.
|
||||
</p>
|
||||
<p>
|
||||
<a href="smith.html">Go to Soundsmith Player</a>
|
||||
</p>
|
||||
<p>
|
||||
Earlier FTA software didn't use Soundsmith. I've built a specialized player
|
||||
specifically for them.
|
||||
</p>
|
||||
<p>
|
||||
<a href="fta.html">Go to FTA Player</a>
|
||||
</p>
|
||||
<p>
|
||||
I have included some quick-and-dirty command-line tools to extract
|
||||
music from FTA demos and other sources inside the extract/ folder.
|
||||
</p>
|
||||
<p>
|
||||
I have documented how I used those tools to reverse engineer demo
|
||||
organization and extract music in the docs/ folder. In particular,
|
||||
the <a href="docs/modulae.html">Modulae demo</a>,
|
||||
the <a href="docs/xmas.html">Xmas demo</a>, and the
|
||||
<a href="docs/nucleus.html">Nucleus demo</a>.
|
||||
</p>
|
||||
<p>
|
||||
I have also documented how the Apple IIgs Ensoniq DOC works, as well as pseudocode
|
||||
on how the Soundsmith player works. You can read about it
|
||||
<a href="docs/player.html">here</a>.
|
||||
</p>
|
||||
<p>
|
||||
Finally, you can check out this project on
|
||||
<a href="https://github.com/mrkite/soundsmith">GitHub</a>.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
47
main.css
Normal file
47
main.css
Normal file
@ -0,0 +1,47 @@
|
||||
body {
|
||||
background: #444;
|
||||
color: #898;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
h2 {
|
||||
font-family: sans-serif;
|
||||
font-variant: small-caps;
|
||||
background: #898;
|
||||
color: #000;
|
||||
padding-left: 1em;
|
||||
}
|
||||
h3 {
|
||||
font-family: sans-serif;
|
||||
padding-top: 1em;
|
||||
padding-left: 1em;
|
||||
margin: 0;
|
||||
}
|
||||
p {
|
||||
font-family: sans-serif;
|
||||
padding-left: 1em;
|
||||
max-width: 80ch;
|
||||
}
|
||||
div {
|
||||
font-family: sans-serif;
|
||||
padding-left: 1em;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
color: #cdc;
|
||||
cursor: pointer;
|
||||
}
|
||||
button {
|
||||
border: 3px double #898;
|
||||
background: #222;
|
||||
color: #898;
|
||||
}
|
||||
#songlist div {
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
#songlist div:hover {
|
||||
background: #888;
|
||||
color: #cdc;
|
||||
}
|
15
smith.html
Normal file
15
smith.html
Normal file
@ -0,0 +1,15 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Soundsmith Player</title>
|
||||
<script type="text/javascript" src="smith.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="main.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Soundsmith Player</h2>
|
||||
<div>Currently playing: <span id="loaded">-none-</span></div>
|
||||
<div>Pattern: <span id="info">none</span></div>
|
||||
<div id="controls"></div>
|
||||
<h3>Available songs</h3>
|
||||
<div id="songlist"></div>
|
||||
</body>
|
||||
</html>
|
664
smith.js
Normal file
664
smith.js
Normal file
@ -0,0 +1,664 @@
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (_) try {
|
||||
if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [0, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
var _this = this;
|
||||
var Song = (function () {
|
||||
function Song() {
|
||||
}
|
||||
return Song;
|
||||
}());
|
||||
var SoundSmith = (function () {
|
||||
function SoundSmith() {
|
||||
this.player = null;
|
||||
}
|
||||
SoundSmith.prototype.getSongList = function (path) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
return __generator(this, function (_a) {
|
||||
return [2, new Promise(function (resolve) {
|
||||
var req = new XMLHttpRequest();
|
||||
req.open('GET', path, true);
|
||||
req.onload = function () {
|
||||
resolve(JSON.parse(req.responseText));
|
||||
};
|
||||
req.send(null);
|
||||
})];
|
||||
});
|
||||
});
|
||||
};
|
||||
SoundSmith.prototype.open = function (name, music, wb) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var _this = this;
|
||||
var loaded, info, song, _a, wavebank, _b, controls, stop_1, play;
|
||||
return __generator(this, function (_c) {
|
||||
switch (_c.label) {
|
||||
case 0:
|
||||
this.name = name;
|
||||
loaded = document.getElementById('loaded');
|
||||
if (loaded) {
|
||||
loaded.textContent = 'loading...';
|
||||
}
|
||||
info = document.getElementById('info');
|
||||
_a = Handle.bind;
|
||||
return [4, this.load(music)];
|
||||
case 1:
|
||||
song = new (_a.apply(Handle, [void 0, _c.sent()]))();
|
||||
_b = Handle.bind;
|
||||
return [4, this.load(wb)];
|
||||
case 2:
|
||||
wavebank = new (_b.apply(Handle, [void 0, _c.sent()]))();
|
||||
if (this.player) {
|
||||
this.player.stop();
|
||||
}
|
||||
this.player = new Player(song, wavebank, function (cur, max) {
|
||||
if (info) {
|
||||
if (max == 0) {
|
||||
info.textContent = 'none';
|
||||
}
|
||||
else {
|
||||
info.textContent = cur.toString(10) + ' / ' + max.toString(10);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (loaded) {
|
||||
loaded.textContent = name;
|
||||
}
|
||||
controls = document.getElementById('controls');
|
||||
if (controls) {
|
||||
while (controls.firstChild) {
|
||||
controls.removeChild(controls.firstChild);
|
||||
}
|
||||
stop_1 = document.createElement('button');
|
||||
stop_1.textContent = '\u23f9';
|
||||
stop_1.addEventListener('click', function () {
|
||||
if (_this.player) {
|
||||
_this.player.stop();
|
||||
}
|
||||
});
|
||||
controls.appendChild(stop_1);
|
||||
play = document.createElement('button');
|
||||
play.textContent = '\u25b6';
|
||||
play.addEventListener('click', function () {
|
||||
if (_this.player) {
|
||||
_this.player.play();
|
||||
}
|
||||
});
|
||||
controls.appendChild(play);
|
||||
}
|
||||
return [2];
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
SoundSmith.prototype.load = function (file) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
return __generator(this, function (_a) {
|
||||
return [2, new Promise(function (resolve) {
|
||||
var req = new XMLHttpRequest();
|
||||
req.open('GET', file, true);
|
||||
req.responseType = 'arraybuffer';
|
||||
req.onload = function () {
|
||||
if (req.response) {
|
||||
resolve(new Uint8Array(req.response));
|
||||
}
|
||||
};
|
||||
req.send(null);
|
||||
})];
|
||||
});
|
||||
});
|
||||
};
|
||||
return SoundSmith;
|
||||
}());
|
||||
document.addEventListener('DOMContentLoaded', function () { return __awaiter(_this, void 0, void 0, function () {
|
||||
var _this = this;
|
||||
var ss, songs, list, _i, songs_1, song, row;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
ss = new SoundSmith();
|
||||
return [4, ss.getSongList('songs.json')];
|
||||
case 1:
|
||||
songs = _a.sent();
|
||||
list = document.getElementById('songlist');
|
||||
if (!list) {
|
||||
return [2];
|
||||
}
|
||||
for (_i = 0, songs_1 = songs; _i < songs_1.length; _i++) {
|
||||
song = songs_1[_i];
|
||||
row = document.createElement('div');
|
||||
row.dataset.name = song.name;
|
||||
row.dataset.music = song.music;
|
||||
row.dataset.wb = song.wb;
|
||||
row.appendChild(document.createTextNode(song.name));
|
||||
row.addEventListener('click', function (event) { return __awaiter(_this, void 0, void 0, function () {
|
||||
var target;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
target = event.target;
|
||||
return [4, ss.open(target.dataset.name, target.dataset.music, target.dataset.wb)];
|
||||
case 1:
|
||||
_a.sent();
|
||||
return [2];
|
||||
}
|
||||
});
|
||||
}); });
|
||||
list.appendChild(row);
|
||||
}
|
||||
return [2];
|
||||
}
|
||||
});
|
||||
}); });
|
||||
var Player = (function () {
|
||||
function Player(music, wavebank, notice) {
|
||||
var _this = this;
|
||||
this.stereo = true;
|
||||
this.timer = 0;
|
||||
this.tempo = 0;
|
||||
this.curRow = 0;
|
||||
this.curPat = 0;
|
||||
this.orders = [];
|
||||
this.volTable = [];
|
||||
this.rowOffset = 0;
|
||||
this.numInst = 0;
|
||||
this.ticksLeft = 0;
|
||||
this.instruments = [];
|
||||
this.compactTable = new Uint16Array(16);
|
||||
this.stereoTable = new Uint16Array(16);
|
||||
this.curInst = new Uint8Array(16);
|
||||
this.arpeggio = new Uint8Array(16);
|
||||
this.tone = new Uint8Array(16);
|
||||
this.frequencies = [
|
||||
0x0000, 0x0016, 0x0017, 0x0018, 0x001a, 0x001b, 0x001d, 0x001e,
|
||||
0x0020, 0x0022, 0x0024, 0x0026, 0x0029, 0x002b, 0x002e, 0x0031,
|
||||
0x0033, 0x0036, 0x003a, 0x003d, 0x0041, 0x0045, 0x0049, 0x004d,
|
||||
0x0052, 0x0056, 0x005c, 0x0061, 0x0067, 0x006d, 0x0073, 0x007a,
|
||||
0x0081, 0x0089, 0x0091, 0x009a, 0x00a3, 0x00ad, 0x00b7, 0x00c2,
|
||||
0x00ce, 0x00d9, 0x00e6, 0x00f4, 0x0102, 0x0112, 0x0122, 0x0133,
|
||||
0x0146, 0x015a, 0x016f, 0x0184, 0x019b, 0x01b4, 0x01ce, 0x01e9,
|
||||
0x0206, 0x0225, 0x0246, 0x0269, 0x028d, 0x02b4, 0x02dd, 0x0309,
|
||||
0x0337, 0x0368, 0x039c, 0x03d3, 0x040d, 0x044a, 0x048c, 0x04d1,
|
||||
0x051a, 0x0568, 0x05ba, 0x0611, 0x066e, 0x06d0, 0x0737, 0x07a5,
|
||||
0x081a, 0x0895, 0x0918, 0x09a2, 0x0a35, 0x0ad0, 0x0b75, 0x0c23,
|
||||
0x0cdc, 0x0d9f, 0x0e6f, 0x0f4b, 0x1033, 0x112a, 0x122f, 0x1344,
|
||||
0x1469, 0x15a0, 0x16e9, 0x1846, 0x19b7, 0x1b3f, 0x1cde, 0x1e95,
|
||||
0x2066, 0x2254, 0x245e, 0x2688,
|
||||
];
|
||||
this.notice = notice;
|
||||
this.es5503 = new ES5503(function (osc) { _this.irq(osc); });
|
||||
this.loadWavebank(wavebank);
|
||||
music.seek(6);
|
||||
var blockLen = music.r16();
|
||||
this.tempo = music.r16();
|
||||
this.es5503.setFrequency(30, 0xfa);
|
||||
this.es5503.setVolume(30, 0);
|
||||
this.es5503.setPointer(30, 0);
|
||||
this.es5503.setSize(30, 0);
|
||||
this.es5503.setEnabled(0x3c);
|
||||
this.es5503.setControl(30, 8);
|
||||
music.seek(0x2c);
|
||||
for (var i = 0; i < 15; i++) {
|
||||
this.volTable.push(music.r16());
|
||||
music.skip(0x1c);
|
||||
}
|
||||
music.seek(0x1d6);
|
||||
var songLen = music.r16() & 0xff;
|
||||
for (var i = 0; i < songLen; i++) {
|
||||
this.orders.push(music.r8() * 64 * 14);
|
||||
}
|
||||
music.seek(0x258);
|
||||
this.notes = music.read(blockLen);
|
||||
this.effects1 = music.read(blockLen);
|
||||
this.effects2 = music.read(blockLen);
|
||||
for (var i = 0; i < 16; i++) {
|
||||
this.stereoTable[i] = music.r16();
|
||||
}
|
||||
this.rowOffset = this.orders[this.curPat];
|
||||
this.notice(this.curPat + 1, this.orders.length);
|
||||
}
|
||||
Player.prototype.play = function () {
|
||||
var _this = this;
|
||||
try {
|
||||
this.ctx = new AudioContext();
|
||||
}
|
||||
catch (e) {
|
||||
alert('No audio support');
|
||||
return;
|
||||
}
|
||||
this.audioNode = this.ctx.createScriptProcessor(0, 0, 2);
|
||||
this.audioNode.onaudioprocess = function (evt) {
|
||||
_this.render(evt);
|
||||
};
|
||||
this.audioNode.connect(this.ctx.destination);
|
||||
};
|
||||
Player.prototype.stop = function () {
|
||||
if (this.audioNode) {
|
||||
this.audioNode.disconnect();
|
||||
}
|
||||
this.audioNode = undefined;
|
||||
if (this.ctx) {
|
||||
this.ctx.close();
|
||||
}
|
||||
this.ctx = undefined;
|
||||
};
|
||||
Player.prototype.loadWavebank = function (wavebank) {
|
||||
wavebank.seek(0);
|
||||
if (wavebank.r4() == 'GSWV') {
|
||||
var ofs = wavebank.r16();
|
||||
this.numInst = wavebank.r8();
|
||||
wavebank.skip(this.numInst * 10);
|
||||
for (var i = 0; i < this.numInst; i++) {
|
||||
var instLen = (wavebank.r8() + wavebank.r8()) * 6;
|
||||
this.instruments.push(wavebank.read(instLen));
|
||||
}
|
||||
wavebank.seek(ofs);
|
||||
var tbl = new Uint8Array(0x10000);
|
||||
tbl.set(wavebank.read(wavebank.length - ofs));
|
||||
}
|
||||
else {
|
||||
wavebank.seek(0);
|
||||
this.numInst = wavebank.r16() & 0xff;
|
||||
this.es5503.setRam(wavebank.read(0x10000));
|
||||
wavebank.seek(0x10022);
|
||||
for (var i = 0; i < this.numInst; i++) {
|
||||
this.instruments.push(wavebank.read(12));
|
||||
wavebank.skip(0x50);
|
||||
}
|
||||
wavebank.skip(0x3c);
|
||||
for (var i = 0; i < 16; i++) {
|
||||
this.compactTable[i] = wavebank.r16();
|
||||
}
|
||||
}
|
||||
};
|
||||
Player.prototype.render = function (evt) {
|
||||
var sampleRate = evt.outputBuffer.sampleRate;
|
||||
var leftBuf = evt.outputBuffer.getChannelData(0);
|
||||
var rightBuf = evt.outputBuffer.getChannelData(1);
|
||||
for (var i = 0; i < evt.outputBuffer.length; i++) {
|
||||
this.ticksLeft -= 26320;
|
||||
if (this.ticksLeft <= 0) {
|
||||
this.ticksLeft += sampleRate;
|
||||
this.es5503.tick();
|
||||
}
|
||||
var _a = this.es5503.render(), left = _a[0], right = _a[1];
|
||||
if (!this.stereo) {
|
||||
leftBuf[i] = (left + right) * 0.707;
|
||||
rightBuf[i] = leftBuf[i];
|
||||
}
|
||||
else {
|
||||
leftBuf[i] = left;
|
||||
rightBuf[i] = right;
|
||||
}
|
||||
}
|
||||
};
|
||||
Player.prototype.irq = function (osc) {
|
||||
if (osc != 30) {
|
||||
this.es5503.go(osc);
|
||||
return;
|
||||
}
|
||||
this.timer++;
|
||||
if (this.timer == this.tempo) {
|
||||
this.timer = 0;
|
||||
for (var oscillator = 0; oscillator < 14; oscillator++) {
|
||||
var semitone = this.notes[this.rowOffset];
|
||||
if (semitone == 0 || (semitone & 0x80)) {
|
||||
this.rowOffset++;
|
||||
if (semitone == 0x80) {
|
||||
this.es5503.setControl(oscillator * 2, 1);
|
||||
this.es5503.setControl(oscillator * 2 + 1, 1);
|
||||
}
|
||||
else if (semitone == 0x81) {
|
||||
this.curRow = 0x3f;
|
||||
}
|
||||
}
|
||||
else {
|
||||
var fx = this.effects1[this.rowOffset];
|
||||
if (fx & 0xf0) {
|
||||
this.curInst[oscillator] = (fx >> 4) - 1;
|
||||
}
|
||||
var inst = this.curInst[oscillator];
|
||||
var volume = this.volTable[inst] >> 1;
|
||||
fx &= 0xf;
|
||||
if (fx == 0) {
|
||||
this.arpeggio[oscillator] = this.effects2[this.rowOffset];
|
||||
this.tone[oscillator] = semitone;
|
||||
}
|
||||
else {
|
||||
this.arpeggio[oscillator] = 0;
|
||||
if (fx == 3) {
|
||||
volume = this.effects2[this.rowOffset] >> 1;
|
||||
this.es5503.setVolume(oscillator * 2, volume);
|
||||
this.es5503.setVolume(oscillator * 2 + 1, volume);
|
||||
}
|
||||
else if (fx == 6) {
|
||||
volume -= this.effects2[this.rowOffset] >> 1;
|
||||
volume = Math.max(volume, 0);
|
||||
this.es5503.setVolume(oscillator * 2, volume);
|
||||
this.es5503.setVolume(oscillator * 2 + 1, volume);
|
||||
}
|
||||
else if (fx == 5) {
|
||||
volume += this.effects2[this.rowOffset] >> 1;
|
||||
volume = Math.min(volume, 0x7f);
|
||||
this.es5503.setVolume(oscillator * 2, volume);
|
||||
this.es5503.setVolume(oscillator * 2 + 1, volume);
|
||||
}
|
||||
else if (fx == 0xf) {
|
||||
this.tempo = this.effects2[this.rowOffset];
|
||||
}
|
||||
}
|
||||
var addr = oscillator * 2;
|
||||
this.es5503.stop(addr);
|
||||
this.es5503.stop(addr + 1);
|
||||
if (inst < this.numInst) {
|
||||
var x = 0;
|
||||
while (this.instruments[inst][x] < semitone) {
|
||||
x += 6;
|
||||
}
|
||||
var oscAptr = this.instruments[inst][x + 1];
|
||||
var oscAsiz = this.instruments[inst][x + 2];
|
||||
var oscActl = this.instruments[inst][x + 3] & 0xf;
|
||||
if (this.stereoTable[oscillator]) {
|
||||
oscActl |= 0x10;
|
||||
}
|
||||
while (this.instruments[inst][x] != 0x7f) {
|
||||
x += 6;
|
||||
}
|
||||
x += 6;
|
||||
while (this.instruments[inst][x] < semitone) {
|
||||
x += 6;
|
||||
}
|
||||
var oscBptr = this.instruments[inst][x + 1];
|
||||
var oscBsiz = this.instruments[inst][x + 2];
|
||||
var oscBctl = this.instruments[inst][x + 3] & 0xf;
|
||||
if (this.stereoTable[oscillator]) {
|
||||
oscBctl |= 0x10;
|
||||
}
|
||||
var freq = this.frequencies[semitone] >>
|
||||
this.compactTable[inst];
|
||||
this.es5503.setFrequency(addr, freq);
|
||||
this.es5503.setFrequency(addr + 1, freq);
|
||||
this.es5503.setVolume(addr, volume);
|
||||
this.es5503.setVolume(addr + 1, volume);
|
||||
this.es5503.setPointer(addr, oscAptr);
|
||||
this.es5503.setPointer(addr + 1, oscBptr);
|
||||
this.es5503.setSize(addr, oscAsiz);
|
||||
this.es5503.setSize(addr + 1, oscBsiz);
|
||||
this.es5503.setControl(addr, oscActl);
|
||||
this.es5503.setControl(addr + 1, oscBctl);
|
||||
}
|
||||
this.rowOffset++;
|
||||
}
|
||||
}
|
||||
this.curRow++;
|
||||
if (this.curRow < 0x40) {
|
||||
return;
|
||||
}
|
||||
this.curRow = 0;
|
||||
this.curPat++;
|
||||
if (this.curPat < this.orders.length) {
|
||||
this.notice(this.curPat + 1, this.orders.length);
|
||||
this.rowOffset = this.orders[this.curPat];
|
||||
return;
|
||||
}
|
||||
this.notice(0, 0);
|
||||
this.stop();
|
||||
return;
|
||||
}
|
||||
else {
|
||||
for (var oscillator = 0; oscillator < 14; oscillator++) {
|
||||
var a = this.arpeggio[oscillator];
|
||||
if (a) {
|
||||
switch (this.timer % 6) {
|
||||
case 1:
|
||||
case 4:
|
||||
this.tone[oscillator] += a >> 4;
|
||||
break;
|
||||
case 2:
|
||||
case 5:
|
||||
this.tone[oscillator] += a & 0xf;
|
||||
break;
|
||||
case 0:
|
||||
case 3:
|
||||
this.tone[oscillator] -= a >> 4;
|
||||
this.tone[oscillator] -= a & 0xf;
|
||||
break;
|
||||
}
|
||||
var freq = this.frequencies[this.tone[oscillator]] >>
|
||||
this.compactTable[oscillator];
|
||||
var addr = oscillator * 2;
|
||||
this.es5503.setFrequency(addr, freq);
|
||||
this.es5503.setFrequency(addr + 1, freq);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return Player;
|
||||
}());
|
||||
var Mode;
|
||||
(function (Mode) {
|
||||
Mode[Mode["freeRun"] = 0] = "freeRun";
|
||||
Mode[Mode["oneShot"] = 1] = "oneShot";
|
||||
Mode[Mode["sync"] = 2] = "sync";
|
||||
Mode[Mode["swap"] = 3] = "swap";
|
||||
})(Mode || (Mode = {}));
|
||||
var Oscillator = (function () {
|
||||
function Oscillator() {
|
||||
this.pointer = 0;
|
||||
this.frequency = 0;
|
||||
this.size = 0;
|
||||
this.control = 1;
|
||||
this.volume = 0;
|
||||
this.data = 0;
|
||||
this.resolution = 0;
|
||||
this.accumulator = 0;
|
||||
this.ptr = 0;
|
||||
this.shift = 9;
|
||||
this.max = 0xff;
|
||||
}
|
||||
return Oscillator;
|
||||
}());
|
||||
var ES5503 = (function () {
|
||||
function ES5503(irq) {
|
||||
this.waveTable = new Float32Array(0x10000);
|
||||
this.oscillators = [];
|
||||
this.enabled = 0;
|
||||
this.waveSizes = [
|
||||
0x100, 0x200, 0x400, 0x800, 0x1000, 0x2000, 0x4000, 0x8000,
|
||||
];
|
||||
this.waveMasks = [
|
||||
0x1ff00, 0x1fe00, 0x1fc00, 0x1f800, 0x1f000, 0x1e000, 0x1c000, 0x18000,
|
||||
];
|
||||
this.irq = irq;
|
||||
for (var i = 0; i < 32; i++) {
|
||||
this.oscillators.push(new Oscillator());
|
||||
}
|
||||
}
|
||||
ES5503.prototype.setEnabled = function (enabled) {
|
||||
this.enabled = enabled >> 1;
|
||||
};
|
||||
ES5503.prototype.setRam = function (bank) {
|
||||
for (var i = 0; i < bank.length; i++) {
|
||||
this.waveTable[i] = (bank[i] - 128) / 128;
|
||||
}
|
||||
};
|
||||
ES5503.prototype.setFrequency = function (osc, freq) {
|
||||
this.oscillators[osc].frequency = freq;
|
||||
};
|
||||
ES5503.prototype.setVolume = function (osc, vol) {
|
||||
this.oscillators[osc].volume = vol / 127;
|
||||
};
|
||||
ES5503.prototype.setPointer = function (osc, ptr) {
|
||||
this.oscillators[osc].pointer = ptr << 8;
|
||||
this.recalc(osc);
|
||||
};
|
||||
ES5503.prototype.setSize = function (osc, size) {
|
||||
this.oscillators[osc].size = (size >> 3) & 7;
|
||||
this.oscillators[osc].resolution = size & 7;
|
||||
this.recalc(osc);
|
||||
};
|
||||
ES5503.prototype.setControl = function (osc, ctl) {
|
||||
var prev = this.oscillators[osc].control & 1;
|
||||
this.oscillators[osc].control = ctl;
|
||||
var mode = (ctl >> 1) & 3;
|
||||
if (!(ctl & 1) && prev) {
|
||||
if (mode == Mode.sync) {
|
||||
this.oscillators[osc ^ 1].control &= ~1;
|
||||
this.oscillators[osc ^ 1].accumulator = 0;
|
||||
}
|
||||
this.oscillators[osc].accumulator = 0;
|
||||
}
|
||||
};
|
||||
ES5503.prototype.stop = function (osc) {
|
||||
this.oscillators[osc].control &= 0xf7;
|
||||
this.oscillators[osc].control |= 1;
|
||||
this.oscillators[osc].accumulator = 0;
|
||||
};
|
||||
ES5503.prototype.go = function (osc) {
|
||||
this.oscillators[osc].control &= ~1;
|
||||
};
|
||||
ES5503.prototype.tick = function () {
|
||||
for (var osc = 0; osc <= this.enabled; osc++) {
|
||||
var cur = this.oscillators[osc];
|
||||
if (!(cur.control & 1)) {
|
||||
var base = cur.accumulator >> cur.shift;
|
||||
var ofs = (base & cur.max) + cur.ptr;
|
||||
cur.data = this.waveTable[ofs] * cur.volume;
|
||||
cur.accumulator += cur.frequency;
|
||||
if (this.waveTable[ofs] == -1) {
|
||||
this.halted(osc, true);
|
||||
}
|
||||
else if (base >= cur.max) {
|
||||
this.halted(osc, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
ES5503.prototype.render = function () {
|
||||
var left = 0;
|
||||
var right = 0;
|
||||
for (var osc = 0; osc <= this.enabled; osc++) {
|
||||
var cur = this.oscillators[osc];
|
||||
if (!(cur.control & 1)) {
|
||||
if (cur.control & 0x10) {
|
||||
right += cur.data;
|
||||
}
|
||||
else {
|
||||
left += cur.data;
|
||||
}
|
||||
}
|
||||
}
|
||||
var spread = (this.enabled - 2) / 4;
|
||||
return [left / spread, right / spread];
|
||||
};
|
||||
ES5503.prototype.recalc = function (osc) {
|
||||
var cur = this.oscillators[osc];
|
||||
cur.shift = (cur.resolution + 9) - cur.size;
|
||||
cur.ptr = cur.pointer & this.waveMasks[cur.size];
|
||||
cur.max = this.waveSizes[cur.size] - 1;
|
||||
};
|
||||
ES5503.prototype.halted = function (osc, interrupted) {
|
||||
var cur = this.oscillators[osc];
|
||||
var mode = (cur.control >> 1) & 3;
|
||||
if (interrupted || mode != Mode.freeRun) {
|
||||
cur.control |= 1;
|
||||
}
|
||||
else {
|
||||
var base = (cur.accumulator >> cur.shift) - cur.max;
|
||||
cur.accumulator = Math.max(base, 0) << cur.shift;
|
||||
}
|
||||
if (mode == Mode.swap) {
|
||||
var swap = this.oscillators[osc ^ 1];
|
||||
swap.control &= ~1;
|
||||
swap.accumulator = 0;
|
||||
}
|
||||
if (cur.control & 8) {
|
||||
this.irq(osc);
|
||||
}
|
||||
};
|
||||
return ES5503;
|
||||
}());
|
||||
var Handle = (function () {
|
||||
function Handle(data) {
|
||||
this.pos = 0;
|
||||
this.data = data;
|
||||
this.length = data.length;
|
||||
}
|
||||
Handle.prototype.eof = function () {
|
||||
return this.pos >= this.length;
|
||||
};
|
||||
Handle.prototype.r8 = function () {
|
||||
return this.data[this.pos++];
|
||||
};
|
||||
Handle.prototype.r16 = function () {
|
||||
var v = this.data[this.pos++];
|
||||
v |= this.data[this.pos++] << 8;
|
||||
return v;
|
||||
};
|
||||
Handle.prototype.r24 = function () {
|
||||
var v = this.data[this.pos++];
|
||||
v |= this.data[this.pos++] << 8;
|
||||
v |= this.data[this.pos++] << 16;
|
||||
return v;
|
||||
};
|
||||
Handle.prototype.r32 = function () {
|
||||
var v = this.data[this.pos++];
|
||||
v |= this.data[this.pos++] << 8;
|
||||
v |= this.data[this.pos++] << 16;
|
||||
v |= this.data[this.pos++] << 24;
|
||||
return v >>> 0;
|
||||
};
|
||||
Handle.prototype.r4 = function () {
|
||||
var r = '';
|
||||
for (var i = 0; i < 4; i++) {
|
||||
r += String.fromCharCode(this.data[this.pos++]);
|
||||
}
|
||||
return r;
|
||||
};
|
||||
Handle.prototype.seek = function (pos) {
|
||||
this.pos = pos;
|
||||
};
|
||||
Handle.prototype.skip = function (len) {
|
||||
this.pos += len;
|
||||
};
|
||||
Handle.prototype.tell = function () {
|
||||
return this.pos;
|
||||
};
|
||||
Handle.prototype.read = function (len) {
|
||||
var oldpos = this.pos;
|
||||
this.pos += len;
|
||||
return this.data.subarray(oldpos, this.pos);
|
||||
};
|
||||
return Handle;
|
||||
}());
|
||||
//# sourceMappingURL=smith.js.map
|
35
songs.json
Normal file
35
songs.json
Normal file
@ -0,0 +1,35 @@
|
||||
[
|
||||
{"name": "Modulae - Intro",
|
||||
"music": "songs/modulae/intro.song",
|
||||
"wb": "songs/modulae/intro.wb"},
|
||||
{"name": "Modulae - Demo",
|
||||
"music": "songs/modulae/demo.song",
|
||||
"wb": "songs/modulae/demo.wb"},
|
||||
{"name": "Xmas Demo - Loading",
|
||||
"music": "songs/xmas/loading.song",
|
||||
"wb": "songs/xmas/loading.wb"},
|
||||
{"name": "Xmas Demo - Menu",
|
||||
"music": "songs/xmas/main.song",
|
||||
"wb": "songs/xmas/main.wb"},
|
||||
{"name": "Xmas Demo - Bullwinkle: The Sequel",
|
||||
"music": "songs/xmas/section1.song",
|
||||
"wb": "songs/xmas/section1.wb"},
|
||||
{"name": "Xmas Demo - The Split Demo",
|
||||
"music": "songs/xmas/section2.song",
|
||||
"wb": "songs/xmas/section2.wb"},
|
||||
{"name": "Xmas Demo - Starwar Fractured Tale",
|
||||
"music": "songs/xmas/section3.song",
|
||||
"wb": "songs/xmas/section2.wb"},
|
||||
{"name": "Xmas Demo - Christmas Gifts",
|
||||
"music": "songs/xmas/section4.song",
|
||||
"wb": "songs/xmas/section4.wb"},
|
||||
{"name": "Xmas Demo - Hidden Track",
|
||||
"music": "songs/xmas/section8.song",
|
||||
"wb": "songs/xmas/section8.wb"},
|
||||
{"name": "Bulla Demo",
|
||||
"music": "songs/bulla/music.song",
|
||||
"wb": "songs/bulla/music.wb"},
|
||||
{"name": "Soundsmith Intro",
|
||||
"music": "songs/ss/intro.song",
|
||||
"wb": "songs/ss/intro.wb"}
|
||||
]
|
158
src/es5503.ts
Normal file
158
src/es5503.ts
Normal file
@ -0,0 +1,158 @@
|
||||
/** Copyright 2017 Sean Kasun */
|
||||
|
||||
enum Mode {
|
||||
freeRun = 0,
|
||||
oneShot = 1,
|
||||
sync = 2,
|
||||
swap = 3,
|
||||
}
|
||||
|
||||
class Oscillator {
|
||||
public pointer: number = 0;
|
||||
public frequency: number = 0;
|
||||
public size: number = 0;
|
||||
public control: number = 1;
|
||||
public volume: number = 0;
|
||||
public data: number = 0;
|
||||
public resolution: number = 0;
|
||||
public accumulator: number = 0;
|
||||
public ptr: number = 0;
|
||||
public shift: number = 9;
|
||||
public max: number = 0xff;
|
||||
}
|
||||
|
||||
class ES5503 {
|
||||
private waveTable: Float32Array = new Float32Array(0x10000);
|
||||
private oscillators: Oscillator[] = [];
|
||||
private irq: (osc: number) => void;
|
||||
private enabled: number = 0;
|
||||
private waveSizes: number[] = [
|
||||
0x100, 0x200, 0x400, 0x800, 0x1000, 0x2000, 0x4000, 0x8000,
|
||||
];
|
||||
private waveMasks: number[] = [
|
||||
0x1ff00, 0x1fe00, 0x1fc00, 0x1f800, 0x1f000, 0x1e000, 0x1c000, 0x18000,
|
||||
];
|
||||
|
||||
constructor(irq: (osc: number) => void) {
|
||||
this.irq = irq;
|
||||
for (let i: number = 0; i < 32; i++) {
|
||||
this.oscillators.push(new Oscillator());
|
||||
}
|
||||
}
|
||||
|
||||
public setEnabled(enabled: number): void {
|
||||
this.enabled = enabled >> 1;
|
||||
}
|
||||
|
||||
public setRam(bank: Uint8Array): void {
|
||||
for (let i: number = 0; i < bank.length; i++) {
|
||||
this.waveTable[i] = (bank[i] - 128) / 128;
|
||||
}
|
||||
}
|
||||
|
||||
public setFrequency(osc: number, freq: number): void {
|
||||
this.oscillators[osc].frequency = freq;
|
||||
}
|
||||
|
||||
public setVolume(osc: number, vol: number): void {
|
||||
this.oscillators[osc].volume = vol / 127;
|
||||
}
|
||||
|
||||
public setPointer(osc: number, ptr: number): void {
|
||||
this.oscillators[osc].pointer = ptr << 8;
|
||||
this.recalc(osc);
|
||||
}
|
||||
|
||||
public setSize(osc: number, size: number): void {
|
||||
this.oscillators[osc].size = (size >> 3) & 7;
|
||||
this.oscillators[osc].resolution = size & 7;
|
||||
this.recalc(osc);
|
||||
}
|
||||
|
||||
public setControl(osc: number, ctl: number): void {
|
||||
const prev: number = this.oscillators[osc].control & 1;
|
||||
this.oscillators[osc].control = ctl;
|
||||
const mode: Mode = (ctl >> 1) & 3;
|
||||
// newly triggered?
|
||||
if (!(ctl & 1) && prev) {
|
||||
if (mode == Mode.sync) { // trigger pair?
|
||||
this.oscillators[osc ^ 1].control &= ~1;
|
||||
this.oscillators[osc ^ 1].accumulator = 0;
|
||||
}
|
||||
this.oscillators[osc].accumulator = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// halt oscillator without triggering interrupt
|
||||
public stop(osc: number): void {
|
||||
this.oscillators[osc].control &= 0xf7; // clear interrupt bit
|
||||
this.oscillators[osc].control |= 1;
|
||||
this.oscillators[osc].accumulator = 0;
|
||||
}
|
||||
|
||||
// unhalt oscillator without triggering swap
|
||||
public go(osc: number): void {
|
||||
this.oscillators[osc].control &= ~1;
|
||||
}
|
||||
|
||||
public tick(): void {
|
||||
for (let osc: number = 0; osc <= this.enabled; osc++) {
|
||||
const cur: Oscillator = this.oscillators[osc];
|
||||
if (!(cur.control & 1)) { // running?
|
||||
const base: number = cur.accumulator >> cur.shift;
|
||||
const ofs: number = (base & cur.max) + cur.ptr;
|
||||
cur.data = this.waveTable[ofs] * cur.volume;
|
||||
|
||||
cur.accumulator += cur.frequency;
|
||||
if (this.waveTable[ofs] == -1) { // same as 0 in the data
|
||||
this.halted(osc, true);
|
||||
} else if (base >= cur.max) {
|
||||
this.halted(osc, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public render(): [number, number] {
|
||||
let left: number = 0;
|
||||
let right: number = 0;
|
||||
for (let osc: number = 0; osc <= this.enabled; osc++) {
|
||||
const cur: Oscillator = this.oscillators[osc];
|
||||
if (!(cur.control & 1)) {
|
||||
if (cur.control & 0x10) {
|
||||
right += cur.data;
|
||||
} else {
|
||||
left += cur.data;
|
||||
}
|
||||
}
|
||||
}
|
||||
const spread: number = (this.enabled - 2) / 4;
|
||||
return [left / spread, right / spread];
|
||||
}
|
||||
|
||||
private recalc(osc: number): void {
|
||||
const cur: Oscillator = this.oscillators[osc];
|
||||
cur.shift = (cur.resolution + 9) - cur.size;
|
||||
cur.ptr = cur.pointer & this.waveMasks[cur.size];
|
||||
cur.max = this.waveSizes[cur.size] - 1;
|
||||
}
|
||||
|
||||
private halted(osc: number, interrupted: boolean) {
|
||||
const cur: Oscillator = this.oscillators[osc];
|
||||
const mode: Mode = (cur.control >> 1) & 3;
|
||||
if (interrupted || mode != Mode.freeRun) {
|
||||
cur.control |= 1; // halt oscillator
|
||||
} else {
|
||||
const base: number = (cur.accumulator >> cur.shift) - cur.max;
|
||||
cur.accumulator = Math.max(base, 0) << cur.shift;
|
||||
}
|
||||
if (mode == Mode.swap) {
|
||||
const swap: Oscillator = this.oscillators[osc ^ 1];
|
||||
swap.control &= ~1; // enable pair
|
||||
swap.accumulator = 0;
|
||||
}
|
||||
if (cur.control & 8) { // should we interrupt?
|
||||
this.irq(osc);
|
||||
}
|
||||
}
|
||||
}
|
110
src/fta.ts
Normal file
110
src/fta.ts
Normal file
@ -0,0 +1,110 @@
|
||||
/** Copyright 2017 Sean Kasun */
|
||||
|
||||
class Song {
|
||||
public name: string;
|
||||
public music: string;
|
||||
public wb: string;
|
||||
public inst: string;
|
||||
public delta: number;
|
||||
}
|
||||
|
||||
class FTA {
|
||||
private name: string;
|
||||
private player: FTAPlayer | null = null;
|
||||
|
||||
public async getSongList(path: string): Promise<Song[]> {
|
||||
return new Promise<Song[]>((resolve: (songs: Song[]) => void) => {
|
||||
const req: XMLHttpRequest = new XMLHttpRequest();
|
||||
req.open('GET', path, true);
|
||||
req.onload = () => {
|
||||
resolve(JSON.parse(req.responseText));
|
||||
};
|
||||
req.send(null);
|
||||
});
|
||||
}
|
||||
|
||||
public async open(name: string, music: string, wb: string,
|
||||
inst: string, delta: number): Promise<void> {
|
||||
this.name = name;
|
||||
const loaded: HTMLElement | null = document.getElementById('loaded');
|
||||
if (loaded) {
|
||||
loaded.textContent = 'loading...';
|
||||
}
|
||||
const song: Handle = new Handle(await this.load(music));
|
||||
const wavebank: Handle = new Handle(await this.load(wb));
|
||||
const instdef: Handle = new Handle(await this.load(inst));
|
||||
|
||||
if (this.player) {
|
||||
this.player.stop();
|
||||
}
|
||||
this.player = new FTAPlayer(song, wavebank, instdef, delta);
|
||||
if (loaded) {
|
||||
loaded.textContent = name;
|
||||
}
|
||||
|
||||
const controls: HTMLElement | null = document.getElementById('controls');
|
||||
if (controls) {
|
||||
while (controls.firstChild) {
|
||||
controls.removeChild(controls.firstChild);
|
||||
}
|
||||
const stop: HTMLButtonElement = document.createElement('button');
|
||||
stop.textContent = '\u23f9';
|
||||
stop.addEventListener('click', () => {
|
||||
if (this.player) {
|
||||
this.player.stop();
|
||||
}
|
||||
});
|
||||
controls.appendChild(stop);
|
||||
const play: HTMLButtonElement = document.createElement('button');
|
||||
play.textContent = '\u25b6';
|
||||
play.addEventListener('click', () => {
|
||||
if (this.player) {
|
||||
this.player.play();
|
||||
}
|
||||
});
|
||||
controls.appendChild(play);
|
||||
}
|
||||
}
|
||||
|
||||
private async load(file: string): Promise<Uint8Array> {
|
||||
return new Promise<Uint8Array>((resolve) => {
|
||||
const req: XMLHttpRequest = new XMLHttpRequest();
|
||||
req.open('GET', file, true);
|
||||
req.responseType = 'arraybuffer';
|
||||
|
||||
req.onload = () => {
|
||||
if (req.response) {
|
||||
resolve(new Uint8Array(req.response));
|
||||
}
|
||||
};
|
||||
req.send(null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const fta: FTA = new FTA();
|
||||
const songs: Song[] = await fta.getSongList('ftasongs.json');
|
||||
const list: HTMLElement | null = document.getElementById('songlist');
|
||||
if (!list) {
|
||||
return;
|
||||
}
|
||||
for (const song of songs) {
|
||||
const row: HTMLElement = document.createElement('div');
|
||||
row.dataset.name = song.name;
|
||||
row.dataset.music = song.music;
|
||||
row.dataset.wb = song.wb;
|
||||
row.dataset.inst = song.inst;
|
||||
row.dataset.delta = song.delta.toString(10);
|
||||
row.appendChild(document.createTextNode(song.name));
|
||||
row.addEventListener('click', async (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
await fta.open(target.dataset.name as string,
|
||||
target.dataset.music as string,
|
||||
target.dataset.wb as string,
|
||||
target.dataset.inst as string,
|
||||
parseInt(target.dataset.delta as string, 10));
|
||||
});
|
||||
list.appendChild(row);
|
||||
}
|
||||
});
|
169
src/ftaplayer.ts
Normal file
169
src/ftaplayer.ts
Normal file
@ -0,0 +1,169 @@
|
||||
/** Copyright 2017 Sean Kasun */
|
||||
|
||||
interface Note {
|
||||
freq: number;
|
||||
time: number;
|
||||
}
|
||||
|
||||
class Channel {
|
||||
public ticks: number = 1;
|
||||
public offset: number = 0;
|
||||
public pos: number = 0;
|
||||
public notes: Note[] = [];
|
||||
public osc: number = 0;
|
||||
// instrument
|
||||
public pointer: number = 0;
|
||||
public size: number = 0;
|
||||
public volume: number = 0;
|
||||
public panning: number = 0;
|
||||
public control: number = 0;
|
||||
public freq: number = 0;
|
||||
}
|
||||
|
||||
class FTAPlayer {
|
||||
private stereo: boolean = true;
|
||||
private es5503: ES5503;
|
||||
private ctx?: AudioContext;
|
||||
private audioNode?: ScriptProcessorNode;
|
||||
private ticksLeft: number = 0;
|
||||
private channels: Channel[] = [];
|
||||
private active: number[] = [];
|
||||
|
||||
constructor(music: Handle, wavebank: Handle, inst: Handle, delta: number) {
|
||||
this.es5503 = new ES5503((osc: number): void => { this.irq(osc); });
|
||||
|
||||
wavebank.seek(0);
|
||||
this.es5503.setRam(wavebank.read(wavebank.length));
|
||||
this.es5503.setEnabled(0x3e);
|
||||
|
||||
for (let i: number = 0; i < 32; i++) {
|
||||
this.channels.push(new Channel());
|
||||
}
|
||||
|
||||
inst.seek(0);
|
||||
let base: number = inst.r16();
|
||||
while (base != 0xffff) {
|
||||
const ch: number = inst.r8();
|
||||
this.active.push(ch);
|
||||
this.channels[ch].osc = ch;
|
||||
this.channels[ch].pointer = inst.r8();
|
||||
this.channels[ch].size = inst.r8();
|
||||
this.channels[ch].volume = inst.r8();
|
||||
this.channels[ch].panning = inst.r8();
|
||||
this.channels[ch].control = inst.r8();
|
||||
music.seek(base - delta);
|
||||
const channelLen: number = music.r16();
|
||||
for (let i: number = 0; i < channelLen; i++) {
|
||||
const freq: number = music.r16();
|
||||
const time: number = music.r8();
|
||||
this.channels[ch].notes.push({freq, time});
|
||||
}
|
||||
base = inst.r16();
|
||||
}
|
||||
|
||||
inst.seek(0x46);
|
||||
this.es5503.setFrequency(31, inst.r16());
|
||||
this.es5503.setControl(31, 8); // freerun + interrupt - halt
|
||||
}
|
||||
|
||||
public play(): void {
|
||||
try {
|
||||
this.ctx = new AudioContext();
|
||||
} catch (e) {
|
||||
alert('No audio support');
|
||||
return;
|
||||
}
|
||||
this.audioNode = this.ctx.createScriptProcessor(0, 0, 2);
|
||||
this.audioNode.onaudioprocess = (evt: AudioProcessingEvent) => {
|
||||
this.render(evt);
|
||||
};
|
||||
this.audioNode.connect(this.ctx.destination);
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
if (this.audioNode) {
|
||||
this.audioNode.disconnect();
|
||||
}
|
||||
this.audioNode = undefined;
|
||||
if (this.ctx) {
|
||||
this.ctx.close();
|
||||
}
|
||||
this.ctx = undefined;
|
||||
}
|
||||
|
||||
private render(evt: AudioProcessingEvent): void {
|
||||
const sampleRate: number = evt.outputBuffer.sampleRate;
|
||||
const leftBuf: Float32Array = evt.outputBuffer.getChannelData(0);
|
||||
const rightBuf: Float32Array = evt.outputBuffer.getChannelData(1);
|
||||
|
||||
for (let i: number = 0; i < evt.outputBuffer.length; i++) {
|
||||
// Oscillators update at 26320 Hz
|
||||
this.ticksLeft -= 26320;
|
||||
if (this.ticksLeft <= 0) {
|
||||
this.ticksLeft += sampleRate;
|
||||
this.es5503.tick();
|
||||
}
|
||||
|
||||
const [left, right] = this.es5503.render();
|
||||
if (!this.stereo) { // mix down to mono
|
||||
leftBuf[i] = (left + right) * 0.707;
|
||||
rightBuf[i] = leftBuf[i];
|
||||
} else {
|
||||
leftBuf[i] = left;
|
||||
rightBuf[i] = right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private irq(osc: number): void {
|
||||
if (osc != 31) { // not a timer
|
||||
const ch: Channel = this.channels[osc & 0xfc];
|
||||
this.noteOff(ch);
|
||||
this.noteOn(ch);
|
||||
} else {
|
||||
for (const c of this.active) {
|
||||
const ch: Channel = this.channels[c];
|
||||
ch.ticks--;
|
||||
if (ch.ticks == 0) {
|
||||
this.noteOff(ch);
|
||||
ch.ticks = ch.notes[ch.pos].time;
|
||||
ch.freq = ch.notes[ch.pos].freq;
|
||||
ch.pos++;
|
||||
if (ch.pos >= ch.notes.length) {
|
||||
ch.pos = 0;
|
||||
}
|
||||
this.noteOn(ch);
|
||||
ch.osc ^= 2; // swap oscillator pairs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private noteOn(ch: Channel): void {
|
||||
this.es5503.setFrequency(ch.osc, ch.freq * 2);
|
||||
this.es5503.setFrequency(ch.osc + 1, ch.freq * 2); // pair
|
||||
const vol: number = ch.volume & 0xf;
|
||||
this.es5503.setVolume(ch.osc, vol << 3); // scale to ch.master
|
||||
this.es5503.setVolume(ch.osc + 1, vol << 3); // scale to ch.master
|
||||
this.es5503.setVolume(ch.osc ^ 2, vol << 3);
|
||||
this.es5503.setVolume((ch.osc ^ 2) + 1, vol << 3);
|
||||
this.es5503.setPointer(ch.osc, ch.pointer);
|
||||
this.es5503.setPointer(ch.osc + 1, ch.pointer);
|
||||
this.es5503.setSize(ch.osc, ch.size);
|
||||
this.es5503.setSize(ch.osc + 1, ch.size);
|
||||
let a: number = 2; // one shot left
|
||||
let b: number = 0x12; // one shot right
|
||||
if (ch.panning == 0) { // pan left
|
||||
b = 2;
|
||||
} else if (ch.panning == 1) { // pan right
|
||||
a = 0x12;
|
||||
}
|
||||
this.es5503.setControl(ch.osc, a | ch.control);
|
||||
this.es5503.setControl(ch.osc + 1, b | ch.control);
|
||||
}
|
||||
|
||||
private noteOff(ch: Channel): void {
|
||||
this.es5503.setControl(ch.osc, 7); // halt + swap - interrupt
|
||||
this.es5503.setControl(ch.osc + 1, 7); // pair
|
||||
}
|
||||
}
|
57
src/handle.ts
Normal file
57
src/handle.ts
Normal file
@ -0,0 +1,57 @@
|
||||
/** Copyright 2017 Sean Kasun */
|
||||
|
||||
class Handle {
|
||||
public length: number;
|
||||
private data: Uint8Array;
|
||||
private pos: number = 0;
|
||||
|
||||
constructor(data: Uint8Array) {
|
||||
this.data = data;
|
||||
this.length = data.length;
|
||||
}
|
||||
public eof(): boolean {
|
||||
return this.pos >= this.length;
|
||||
}
|
||||
public r8(): number {
|
||||
return this.data[this.pos++];
|
||||
}
|
||||
public r16(): number {
|
||||
let v: number = this.data[this.pos++];
|
||||
v |= this.data[this.pos++] << 8;
|
||||
return v;
|
||||
}
|
||||
public r24(): number {
|
||||
let v: number = this.data[this.pos++];
|
||||
v |= this.data[this.pos++] << 8;
|
||||
v |= this.data[this.pos++] << 16;
|
||||
return v;
|
||||
}
|
||||
public r32(): number {
|
||||
let v: number = this.data[this.pos++];
|
||||
v |= this.data[this.pos++] << 8;
|
||||
v |= this.data[this.pos++] << 16;
|
||||
v |= this.data[this.pos++] << 24;
|
||||
return v >>> 0; // force 32-bit unsigned
|
||||
}
|
||||
public r4(): string {
|
||||
let r: string = '';
|
||||
for (let i: number = 0; i < 4; i++) {
|
||||
r += String.fromCharCode(this.data[this.pos++]);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
public seek(pos: number): void {
|
||||
this.pos = pos;
|
||||
}
|
||||
public skip(len: number): void {
|
||||
this.pos += len;
|
||||
}
|
||||
public tell(): number {
|
||||
return this.pos;
|
||||
}
|
||||
public read(len: number): Uint8Array {
|
||||
const oldpos: number = this.pos;
|
||||
this.pos += len;
|
||||
return this.data.subarray(oldpos, this.pos);
|
||||
}
|
||||
}
|
300
src/player.ts
Normal file
300
src/player.ts
Normal file
@ -0,0 +1,300 @@
|
||||
/** Copyright 2017 Sean Kasun */
|
||||
|
||||
class Player {
|
||||
private es5503: ES5503;
|
||||
private ctx?: AudioContext;
|
||||
private audioNode?: ScriptProcessorNode;
|
||||
private stereo: boolean = true;
|
||||
|
||||
private timer: number = 0;
|
||||
private tempo: number = 0;
|
||||
private curRow: number = 0;
|
||||
private curPat: number = 0;
|
||||
private orders: number[] = [];
|
||||
private volTable: number[] = [];
|
||||
private rowOffset: number = 0;
|
||||
private numInst: number = 0;
|
||||
private ticksLeft: number = 0;
|
||||
private notes: Uint8Array;
|
||||
private effects1: Uint8Array;
|
||||
private effects2: Uint8Array;
|
||||
private instruments: Uint8Array[] = [];
|
||||
private compactTable: Uint16Array = new Uint16Array(16);
|
||||
private stereoTable: Uint16Array = new Uint16Array(16);
|
||||
private curInst: Uint8Array = new Uint8Array(16);
|
||||
private arpeggio: Uint8Array = new Uint8Array(16);
|
||||
private tone: Uint8Array = new Uint8Array(16);
|
||||
private notice: (pat: number, max: number) => void;
|
||||
private frequencies: number[] = [
|
||||
0x0000, 0x0016, 0x0017, 0x0018, 0x001a, 0x001b, 0x001d, 0x001e,
|
||||
0x0020, 0x0022, 0x0024, 0x0026, 0x0029, 0x002b, 0x002e, 0x0031,
|
||||
0x0033, 0x0036, 0x003a, 0x003d, 0x0041, 0x0045, 0x0049, 0x004d,
|
||||
0x0052, 0x0056, 0x005c, 0x0061, 0x0067, 0x006d, 0x0073, 0x007a,
|
||||
0x0081, 0x0089, 0x0091, 0x009a, 0x00a3, 0x00ad, 0x00b7, 0x00c2,
|
||||
0x00ce, 0x00d9, 0x00e6, 0x00f4, 0x0102, 0x0112, 0x0122, 0x0133,
|
||||
0x0146, 0x015a, 0x016f, 0x0184, 0x019b, 0x01b4, 0x01ce, 0x01e9,
|
||||
0x0206, 0x0225, 0x0246, 0x0269, 0x028d, 0x02b4, 0x02dd, 0x0309,
|
||||
0x0337, 0x0368, 0x039c, 0x03d3, 0x040d, 0x044a, 0x048c, 0x04d1,
|
||||
0x051a, 0x0568, 0x05ba, 0x0611, 0x066e, 0x06d0, 0x0737, 0x07a5,
|
||||
0x081a, 0x0895, 0x0918, 0x09a2, 0x0a35, 0x0ad0, 0x0b75, 0x0c23,
|
||||
0x0cdc, 0x0d9f, 0x0e6f, 0x0f4b, 0x1033, 0x112a, 0x122f, 0x1344,
|
||||
0x1469, 0x15a0, 0x16e9, 0x1846, 0x19b7, 0x1b3f, 0x1cde, 0x1e95,
|
||||
0x2066, 0x2254, 0x245e, 0x2688,
|
||||
];
|
||||
|
||||
constructor(music: Handle, wavebank: Handle,
|
||||
notice: (pat: number, max: number) => void) {
|
||||
this.notice = notice;
|
||||
this.es5503 = new ES5503((osc: number): void => { this.irq(osc); });
|
||||
|
||||
this.loadWavebank(wavebank);
|
||||
|
||||
music.seek(6);
|
||||
const blockLen: number = music.r16();
|
||||
this.tempo = music.r16();
|
||||
this.es5503.setFrequency(30, 0xfa);
|
||||
this.es5503.setVolume(30, 0);
|
||||
this.es5503.setPointer(30, 0);
|
||||
this.es5503.setSize(30, 0);
|
||||
this.es5503.setEnabled(0x3c);
|
||||
this.es5503.setControl(30, 8); // freerun + interrupts - halt
|
||||
|
||||
music.seek(0x2c);
|
||||
for (let i: number = 0; i < 15; i++) {
|
||||
this.volTable.push(music.r16());
|
||||
music.skip(0x1c);
|
||||
}
|
||||
|
||||
music.seek(0x1d6);
|
||||
const songLen: number = music.r16() & 0xff;
|
||||
for (let i: number = 0; i < songLen; i++) {
|
||||
this.orders.push(music.r8() * 64 * 14);
|
||||
}
|
||||
|
||||
music.seek(0x258);
|
||||
this.notes = music.read(blockLen);
|
||||
this.effects1 = music.read(blockLen);
|
||||
this.effects2 = music.read(blockLen);
|
||||
for (let i: number = 0; i < 16; i++) {
|
||||
this.stereoTable[i] = music.r16();
|
||||
}
|
||||
|
||||
this.rowOffset = this.orders[this.curPat];
|
||||
this.notice(this.curPat + 1, this.orders.length);
|
||||
}
|
||||
|
||||
public play(): void {
|
||||
try {
|
||||
this.ctx = new AudioContext();
|
||||
} catch (e) {
|
||||
alert('No audio support');
|
||||
return;
|
||||
}
|
||||
this.audioNode = this.ctx.createScriptProcessor(0, 0, 2);
|
||||
this.audioNode.onaudioprocess = (evt: AudioProcessingEvent) => {
|
||||
this.render(evt);
|
||||
};
|
||||
this.audioNode.connect(this.ctx.destination);
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
if (this.audioNode) {
|
||||
this.audioNode.disconnect();
|
||||
}
|
||||
this.audioNode = undefined;
|
||||
if (this.ctx) {
|
||||
this.ctx.close();
|
||||
}
|
||||
this.ctx = undefined;
|
||||
}
|
||||
|
||||
private loadWavebank(wavebank: Handle): void {
|
||||
wavebank.seek(0);
|
||||
if (wavebank.r4() == 'GSWV') { // gswv wavebank
|
||||
const ofs: number = wavebank.r16();
|
||||
this.numInst = wavebank.r8();
|
||||
wavebank.skip(this.numInst * 10); // skip instrument names
|
||||
for (let i: number = 0; i < this.numInst; i++) {
|
||||
const instLen: number = (wavebank.r8() + wavebank.r8()) * 6;
|
||||
this.instruments.push(wavebank.read(instLen));
|
||||
}
|
||||
wavebank.seek(ofs);
|
||||
const tbl: Uint8Array = new Uint8Array(0x10000);
|
||||
tbl.set(wavebank.read(wavebank.length - ofs));
|
||||
} else { // regular wavebank
|
||||
wavebank.seek(0);
|
||||
this.numInst = wavebank.r16() & 0xff;
|
||||
this.es5503.setRam(wavebank.read(0x10000));
|
||||
|
||||
wavebank.seek(0x10022);
|
||||
for (let i: number = 0; i < this.numInst; i++) {
|
||||
this.instruments.push(wavebank.read(12));
|
||||
wavebank.skip(0x50);
|
||||
}
|
||||
wavebank.skip(0x3c);
|
||||
for (let i: number = 0; i < 16; i++) {
|
||||
this.compactTable[i] = wavebank.r16();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private render(evt: AudioProcessingEvent): void {
|
||||
const sampleRate: number = evt.outputBuffer.sampleRate;
|
||||
const leftBuf: Float32Array = evt.outputBuffer.getChannelData(0);
|
||||
const rightBuf: Float32Array = evt.outputBuffer.getChannelData(1);
|
||||
|
||||
for (let i: number = 0; i < evt.outputBuffer.length; i++) {
|
||||
// Oscillators update at 26320 Hz
|
||||
this.ticksLeft -= 26320;
|
||||
if (this.ticksLeft <= 0) {
|
||||
this.ticksLeft += sampleRate;
|
||||
this.es5503.tick();
|
||||
}
|
||||
|
||||
const [left, right] = this.es5503.render();
|
||||
if (!this.stereo) { // mix down to mono
|
||||
leftBuf[i] = (left + right) * 0.707;
|
||||
rightBuf[i] = leftBuf[i];
|
||||
} else {
|
||||
leftBuf[i] = left;
|
||||
rightBuf[i] = right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private irq(osc: number): void {
|
||||
if (osc != 30) { // not a timer
|
||||
this.es5503.go(osc);
|
||||
return;
|
||||
}
|
||||
|
||||
this.timer++;
|
||||
if (this.timer == this.tempo) {
|
||||
this.timer = 0;
|
||||
for (let oscillator: number = 0; oscillator < 14; oscillator++) {
|
||||
const semitone: number = this.notes[this.rowOffset];
|
||||
if (semitone == 0 || (semitone & 0x80)) {
|
||||
this.rowOffset++;
|
||||
if (semitone == 0x80) {
|
||||
this.es5503.setControl(oscillator * 2, 1); // halt
|
||||
this.es5503.setControl(oscillator * 2 + 1, 1); // halt pair
|
||||
} else if (semitone == 0x81) {
|
||||
this.curRow = 0x3f;
|
||||
}
|
||||
} else {
|
||||
let fx: number = this.effects1[this.rowOffset];
|
||||
if (fx & 0xf0) { // change instrument?
|
||||
this.curInst[oscillator] = (fx >> 4) - 1;
|
||||
}
|
||||
const inst: number = this.curInst[oscillator];
|
||||
let volume: number = this.volTable[inst] >> 1;
|
||||
fx &= 0xf;
|
||||
if (fx == 0) {
|
||||
this.arpeggio[oscillator] = this.effects2[this.rowOffset];
|
||||
this.tone[oscillator] = semitone;
|
||||
} else {
|
||||
this.arpeggio[oscillator] = 0;
|
||||
if (fx == 3) {
|
||||
volume = this.effects2[this.rowOffset] >> 1;
|
||||
this.es5503.setVolume(oscillator * 2, volume);
|
||||
this.es5503.setVolume(oscillator * 2 + 1, volume);
|
||||
} else if (fx == 6) {
|
||||
volume -= this.effects2[this.rowOffset] >> 1;
|
||||
volume = Math.max(volume, 0);
|
||||
this.es5503.setVolume(oscillator * 2, volume);
|
||||
this.es5503.setVolume(oscillator * 2 + 1, volume);
|
||||
} else if (fx == 5) {
|
||||
volume += this.effects2[this.rowOffset] >> 1;
|
||||
volume = Math.min(volume, 0x7f);
|
||||
this.es5503.setVolume(oscillator * 2, volume);
|
||||
this.es5503.setVolume(oscillator * 2 + 1, volume);
|
||||
} else if (fx == 0xf) {
|
||||
this.tempo = this.effects2[this.rowOffset];
|
||||
}
|
||||
}
|
||||
|
||||
const addr: number = oscillator * 2;
|
||||
this.es5503.stop(addr);
|
||||
this.es5503.stop(addr + 1);
|
||||
if (inst < this.numInst) {
|
||||
let x = 0;
|
||||
while (this.instruments[inst][x] < semitone) {
|
||||
x += 6;
|
||||
}
|
||||
const oscAptr: number = this.instruments[inst][x + 1];
|
||||
const oscAsiz: number = this.instruments[inst][x + 2];
|
||||
let oscActl: number = this.instruments[inst][x + 3] & 0xf;
|
||||
if (this.stereoTable[oscillator]) {
|
||||
oscActl |= 0x10;
|
||||
}
|
||||
while (this.instruments[inst][x] != 0x7f) {
|
||||
x += 6;
|
||||
}
|
||||
x += 6; // skip last
|
||||
while (this.instruments[inst][x] < semitone) {
|
||||
x += 6;
|
||||
}
|
||||
const oscBptr: number = this.instruments[inst][x + 1];
|
||||
const oscBsiz: number = this.instruments[inst][x + 2];
|
||||
let oscBctl: number = this.instruments[inst][x + 3] & 0xf;
|
||||
if (this.stereoTable[oscillator]) {
|
||||
oscBctl |= 0x10;
|
||||
}
|
||||
const freq: number = this.frequencies[semitone] >>
|
||||
this.compactTable[inst];
|
||||
this.es5503.setFrequency(addr, freq);
|
||||
this.es5503.setFrequency(addr + 1, freq); // pair
|
||||
this.es5503.setVolume(addr, volume);
|
||||
this.es5503.setVolume(addr + 1, volume); // pair
|
||||
this.es5503.setPointer(addr, oscAptr);
|
||||
this.es5503.setPointer(addr + 1, oscBptr); // pair
|
||||
this.es5503.setSize(addr, oscAsiz);
|
||||
this.es5503.setSize(addr + 1, oscBsiz); // pair
|
||||
this.es5503.setControl(addr, oscActl);
|
||||
this.es5503.setControl(addr + 1, oscBctl); // pair
|
||||
}
|
||||
this.rowOffset++;
|
||||
}
|
||||
}
|
||||
this.curRow++;
|
||||
if (this.curRow < 0x40) {
|
||||
return;
|
||||
}
|
||||
// advance pattern
|
||||
this.curRow = 0;
|
||||
this.curPat++;
|
||||
if (this.curPat < this.orders.length) {
|
||||
this.notice(this.curPat + 1, this.orders.length);
|
||||
this.rowOffset = this.orders[this.curPat];
|
||||
return;
|
||||
}
|
||||
// stopped
|
||||
this.notice(0, 0);
|
||||
this.stop();
|
||||
return;
|
||||
} else { // between notes. Apply arpeggio
|
||||
for (let oscillator: number = 0; oscillator < 14; oscillator++) {
|
||||
const a: number = this.arpeggio[oscillator];
|
||||
if (a) {
|
||||
switch (this.timer % 6) {
|
||||
case 1: case 4:
|
||||
this.tone[oscillator] += a >> 4;
|
||||
break;
|
||||
case 2: case 5:
|
||||
this.tone[oscillator] += a & 0xf;
|
||||
break;
|
||||
case 0: case 3:
|
||||
this.tone[oscillator] -= a >> 4;
|
||||
this.tone[oscillator] -= a & 0xf;
|
||||
break;
|
||||
}
|
||||
const freq: number = this.frequencies[this.tone[oscillator]] >>
|
||||
this.compactTable[oscillator];
|
||||
const addr: number = oscillator * 2;
|
||||
this.es5503.setFrequency(addr, freq);
|
||||
this.es5503.setFrequency(addr + 1, freq); // pair
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
113
src/smith.ts
Normal file
113
src/smith.ts
Normal file
@ -0,0 +1,113 @@
|
||||
/** Copyright 2017 Sean Kasun */
|
||||
|
||||
class Song {
|
||||
public name: string;
|
||||
public music: string;
|
||||
public wb: string;
|
||||
}
|
||||
|
||||
class SoundSmith {
|
||||
private name: string;
|
||||
private player: Player | null = null;
|
||||
|
||||
public async getSongList(path: string): Promise<Song[]> {
|
||||
return new Promise<Song[]>((resolve: (songs: Song[]) => void) => {
|
||||
const req: XMLHttpRequest = new XMLHttpRequest();
|
||||
req.open('GET', path, true);
|
||||
req.onload = () => {
|
||||
resolve(JSON.parse(req.responseText));
|
||||
};
|
||||
req.send(null);
|
||||
});
|
||||
}
|
||||
|
||||
public async open(name: string, music: string, wb: string): Promise<void> {
|
||||
this.name = name;
|
||||
const loaded: HTMLElement | null = document.getElementById('loaded');
|
||||
if (loaded) {
|
||||
loaded.textContent = 'loading...';
|
||||
}
|
||||
const info: HTMLElement | null = document.getElementById('info');
|
||||
|
||||
const song: Handle = new Handle(await this.load(music));
|
||||
const wavebank: Handle = new Handle(await this.load(wb));
|
||||
|
||||
if (this.player) {
|
||||
this.player.stop();
|
||||
}
|
||||
this.player = new Player(song, wavebank, (cur: number, max: number) => {
|
||||
if (info) {
|
||||
if (max == 0) {
|
||||
info.textContent = 'none';
|
||||
} else {
|
||||
info.textContent = cur.toString(10) + ' / ' + max.toString(10);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (loaded) {
|
||||
loaded.textContent = name;
|
||||
}
|
||||
|
||||
const controls: HTMLElement | null = document.getElementById('controls');
|
||||
if (controls) {
|
||||
while (controls.firstChild) {
|
||||
controls.removeChild(controls.firstChild);
|
||||
}
|
||||
const stop: HTMLButtonElement = document.createElement('button');
|
||||
stop.textContent = '\u23f9';
|
||||
stop.addEventListener('click', () => {
|
||||
if (this.player) {
|
||||
this.player.stop();
|
||||
}
|
||||
});
|
||||
controls.appendChild(stop);
|
||||
const play: HTMLButtonElement = document.createElement('button');
|
||||
play.textContent = '\u25b6';
|
||||
play.addEventListener('click', () => {
|
||||
if (this.player) {
|
||||
this.player.play();
|
||||
}
|
||||
});
|
||||
controls.appendChild(play);
|
||||
}
|
||||
}
|
||||
|
||||
private async load(file: string): Promise<Uint8Array> {
|
||||
return new Promise<Uint8Array>((resolve) => {
|
||||
const req: XMLHttpRequest = new XMLHttpRequest();
|
||||
req.open('GET', file, true);
|
||||
req.responseType = 'arraybuffer';
|
||||
|
||||
req.onload = () => {
|
||||
if (req.response) {
|
||||
resolve(new Uint8Array(req.response));
|
||||
}
|
||||
};
|
||||
req.send(null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const ss: SoundSmith = new SoundSmith();
|
||||
const songs: Song[] = await ss.getSongList('songs.json');
|
||||
const list: HTMLElement | null = document.getElementById('songlist');
|
||||
if (!list) {
|
||||
return;
|
||||
}
|
||||
for (const song of songs) {
|
||||
const row: HTMLElement = document.createElement('div');
|
||||
row.dataset.name = song.name;
|
||||
row.dataset.music = song.music;
|
||||
row.dataset.wb = song.wb;
|
||||
row.appendChild(document.createTextNode(song.name));
|
||||
row.addEventListener('click', async (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
await ss.open(target.dataset.name as string,
|
||||
target.dataset.music as string,
|
||||
target.dataset.wb as string);
|
||||
});
|
||||
list.appendChild(row);
|
||||
}
|
||||
});
|
24
tsconfig.json
Normal file
24
tsconfig.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "system",
|
||||
"noEmitOnError": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"removeComments": true,
|
||||
"strictNullChecks": true,
|
||||
"outFile": "smith.js",
|
||||
"target": "ES5",
|
||||
"lib": ["dom", "es2015"],
|
||||
"sourceMap": true
|
||||
},
|
||||
"files": [
|
||||
"src/smith.ts",
|
||||
"src/player.ts",
|
||||
"src/es5503.ts",
|
||||
"src/handle.ts"
|
||||
]
|
||||
}
|
24
tsconfig_fta.json
Normal file
24
tsconfig_fta.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "system",
|
||||
"noEmitOnError": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"removeComments": true,
|
||||
"strictNullChecks": true,
|
||||
"outFile": "fta.js",
|
||||
"target": "ES5",
|
||||
"lib": ["dom", "es2015"],
|
||||
"sourceMap": true
|
||||
},
|
||||
"files": [
|
||||
"src/fta.ts",
|
||||
"src/ftaplayer.ts",
|
||||
"src/es5503.ts",
|
||||
"src/handle.ts"
|
||||
]
|
||||
}
|
18
tslint.json
Normal file
18
tslint.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"defaultSeverity": "error",
|
||||
"extends": [
|
||||
"tslint:recommended"
|
||||
],
|
||||
"jsRules": {},
|
||||
"rules": {
|
||||
"max-line-length": [true, 80],
|
||||
"no-bitwise": false,
|
||||
"triple-equals": false,
|
||||
"interface-name": false,
|
||||
"quotemark": [true, "single", "avoid-template", "avoid-escape"],
|
||||
"no-console": false,
|
||||
"max-classes-per-file": false,
|
||||
"object-literal-sort-keys": false
|
||||
},
|
||||
"rulesDirectory": []
|
||||
}
|
Loading…
Reference in New Issue
Block a user