initial import

This commit is contained in:
Sean 2017-08-21 11:12:33 -07:00
commit 4f1a6bb8c4
44 changed files with 10865 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
*.map
*.swp
songs/

24
LICENSE Normal file
View 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
View 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
View 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
View 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&#8217;s
Modulae demo, straight from the 2mg disk image.</p></div>
<div class="paragraph"><p>We&#8217;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">&gt;</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&#8212;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&#8212;13 contain the main loader, it&#8217;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">&gt;</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&#8217;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&#8217;s actually the &#8220;And now, FTA Presents&#8230;&#8221; raw audio data.
It&#8217;s stored as an unsigned 8-bit PCM data. It&#8217;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&#8217;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&#8217;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&#8217;t
seem to reference them, so I&#8217;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
View 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
View 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">&gt;</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&#8212;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">&gt;</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">&gt;</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&#8217;t any patterns,
there&#8217;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&#8217;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&#8217;ll also divide up the songdefs file to provide easy access to each song&#8217;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&#8217;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
View 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

File diff suppressed because it is too large Load Diff

482
docs/player.txt Normal file
View 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
View 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&#8217;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">&gt;</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&#8212;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&#8217;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">&gt;</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&#8217;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&#8217;ll ignore it.</p></div>
</div>
</div>
<div class="sect1">
<h2 id="_loading_8230_music">Loading&#8230; 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&#8217;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&#8217;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">&gt;</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&#8217;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&#8217;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&#8217;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&#8217;t sound quite right, so I&#8217;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&#8217;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&#8217;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&#8217;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&#8217;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&#8217;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
View 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
View File

@ -0,0 +1,7 @@
*.o
decrunch
dumptbl
disasm
trimmusic
trimwb
2mg

230
extract/2mg.c Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

47
extract/trimmusic.c Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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": []
}