# Recommended Glib version: at least 2.16
GTK_REQUIRED_VERSION=2.6
GLIB_REQUIRED_VERSION=2.6
-GTK_DOC_REQUIRED_VERSION=1.9
+GTK_DOC_REQUIRED_VERSION=1.12
AC_SUBST(GTK_REQUIRED_VERSION)
AC_SUBST(GLIB_REQUIRED_VERSION)
AC_SUBST(GTK_DOC_REQUIRED_VERSION)
[enable_iliad=no])
AM_CONDITIONAL(TARGET_ILIAD, $TEST "x$enable_iliad" = xyes)
-### BUILD WITHOUT RECENT FILES MANAGER #########################################
-# (to work around a bug on OS X)
-AC_ARG_ENABLE([recent],
- [AS_HELP_STRING([--disable-recent],
- [Omit recent files menu (to work around a bug on OS X])],
- [],
- [enable_recent=yes])
-AS_IF([$TEST "x$enable_recent" = "xyes"],
- [OPEN_RECENT_MENU_ITEM="<menuitem action=\"recent\"/>"],
- [OPEN_RECENT_MENU_ITEM="<!-- <menuitem action=\"recent\"/>-->"])
-AC_SUBST(OPEN_RECENT_MENU_ITEM)
-
### RPM CONFIGURATION ##########################################################
# --enable-rpm requires rpm and rpmbuild
AC_PATH_PROG([RPMBUILD], [rpmbuild], [notfound])
[AC_DEFINE([GSTREAMER_SOUND], [1], [Define to enable sound support with GStreamer])
SOUND_MODULE="gstreamer-0.10 >= 0.10.12"])
+### WHETHER TO GENERATE A .VAPI FILE ##########################################
+# Requires vapigen
+AC_PATH_PROG([VAPIGEN], [vapigen], [notfound])
+AM_CONDITIONAL(BUILDING_VAPI, $TEST "x$VAPIGEN" != xnotfound)
+
### CHECK FOR LIBRARIES #######################################################
# Libraries needed to build Chimara library
interpreters/git/Makefile
tests/Makefile
player/Makefile
-player/chimara.menus
+player/config.py
docs/Makefile
docs/reference/Makefile
docs/reference/version.xml
</para>
<para>
For the complete Blorb specification and tools for Blorb file manipulation, see:
-<ulink role="online-location" url="http://www.eblong.com/zarf/blorb/">http://www.eblong.com/zarf/blorb/</ulink>
+<ulink role="online-location" url="http://eblong.com/zarf/blorb/"/>
</para>
<refsect2 id="chimara-How-This-Works">
<title>How This Works</title>
</reference>
<reference id="chimara-glk-api-spec">
- <title>Glk API Specification, version 0.7.0</title>
+ <title>Glk API Specification, version 0.7.4</title>
<xi:include href="glk-front-matter.sgml"/>
<!-- Chapter 0. Introduction -->
chimara_glk_set_css_from_file
chimara_glk_set_css_from_string
chimara_glk_run
+chimara_glk_run_file
chimara_glk_stop
chimara_glk_wait
chimara_glk_unload_plugin
chimara_if_set_preferred_interpreter
chimara_if_get_preferred_interpreter
chimara_if_run_game
+chimara_if_run_game_file
chimara_if_get_format
chimara_if_get_interpreter
<SUBSECTION Standard>
gestalt_Timer
gestalt_Graphics
gestalt_DrawImage
+gestalt_Sound2
gestalt_Sound
gestalt_SoundVolume
gestalt_SoundNotify
gestalt_SoundMusic
gestalt_GraphicsTransparency
gestalt_DateTime
+gestalt_ResourceStream
+GLK_MODULE_RESOURCE_STREAM
</SECTION>
<SECTION>
evtype_Redraw
evtype_SoundNotify
evtype_Hyperlink
+evtype_VolumeNotify
</SECTION>
<SECTION>
glk_stream_open_memory_uni
glk_stream_open_file
glk_stream_open_file_uni
+glk_stream_open_resource
+glk_stream_open_resource_uni
</SECTION>
<SECTION>
<FILE>glk-sound-channels</FILE>
<TITLE>Creating and Destroying Sound Channels</TITLE>
glk_schannel_create
+glk_schannel_create_ext
glk_schannel_destroy
</SECTION>
<TITLE>Playing Sounds</TITLE>
glk_schannel_play
glk_schannel_play_ext
+glk_schannel_play_multi
glk_schannel_stop
+glk_schannel_pause
+glk_schannel_unpause
glk_schannel_set_volume
+glk_schannel_set_volume_ext
glk_sound_load_hint
</SECTION>
<FILE>glk-sound-testing</FILE>
<TITLE>Testing for Sound Capabilities</TITLE>
GLK_MODULE_SOUND
+GLK_MODULE_SOUND2
</SECTION>
<SECTION>
giblorb_method_DontLoad
giblorb_method_Memory
giblorb_method_FilePos
-giblorb_ID_Snd
giblorb_ID_Exec
+giblorb_ID_Snd
giblorb_ID_Pict
+giblorb_ID_Data
giblorb_ID_Copyright
giblorb_ID_AUTH
giblorb_ID_ANNO
+giblorb_ID_TEXT
+giblorb_ID_BINA
<SUBSECTION Private>
giblorb_make_id
</SECTION>
]>
<referenceinfo>
<title>Glk API Specification</title>
-<subtitle>API version 0.7.2</subtitle>
+<subtitle>API version 0.7.4</subtitle>
<author>
<personname>
<firstname>Andrew</firstname>
<email>erkyrath@eblong.com</email>
</author>
<copyright>
- <year>1998–2011</year>
+ <year>1998–2012</year>
<holder>Andrew Plotkin</holder>
</copyright>
<legalnotice>
</para>
</legalnotice>
<releaseinfo>
-The authors of the Chimara library have adapted this document to better fit the format of a GtkDoc reference manual. They have also added notes specific to Chimara's implementation of the Glk API. The original API specification and further Glk information can be found at: <ulink url="http://www.eblong.com/zarf/glk/">http://www.eblong.com/zarf/glk/</ulink>
+The authors of the Chimara library have adapted this document to better fit the format of a GtkDoc reference manual. They have also added notes specific to Chimara's implementation of the Glk API. The original API specification and further Glk information can be found at: <ulink url="http://eblong.com/zarf/glk/"/>
</releaseinfo>
</referenceinfo>
An IF virtual machine has been designed specifically to go along with Glk. This VM, called Glulx, uses Glk as its interface; each Glk call corresponds to an input/output opcode of the VM.
</para>
<para>
-For more discussion of this approach, see <link linkend="chimara-Glk-and-the-Virtual-Machine">Glk and the Virtual Machine</link>. Glulx is documented at <ulink url="http://www.eblong.com/zarf/glulx/">http://www.eblong.com/zarf/glulx</ulink>.
+For more discussion of this approach, see <link linkend="chimara-Glk-and-the-Virtual-Machine">Glk and the Virtual Machine</link>. Glulx is documented at <ulink url="http://eblong.com/zarf/glulx/">http://eblong.com/zarf/glulx</ulink>.
</para>
<para>
Of course, Glk can be used with other IF systems. The advantage of Glulx is that it provides the game author with direct and complete access to the Glk API. Other IF systems typically have an built-in abstract I/O API, which maps only partially onto Glk. For these systems, Glk tends to be a least-common-denominator interface: highly portable, but not necessarily featureful. (Even if Glk has a feature, it may not be available through the layers of abstraction.)
A resource can also contain two or more channels of sound (stereo data). Do not confuse such in-sound channels with Glk sound channels. A single Glk sound channel suffices to play any sound, even stereo sounds.
</para>
<note><para>
-Again, Blorb is the official resource-storage format of Glk. Sounds in Blorb files can be encoded as AIFF, MOD, or MOD song data. See the Blorb specification for details.
+Again, Blorb is the official resource-storage format of Glk. Sounds in Blorb files can be encoded as Ogg, AIFF, or MOD. See the Blorb specification for details.
</para></note>
</refsect1>
-</refentry>
\ No newline at end of file
+</refentry>
Glulxe: the Glulx VM interpreter
-Version 0.4.###
+Version 0.4.7
Designed by Andrew Plotkin <erkyrath@eblong.com>
http://eblong.com/zarf/glulx/index.html
* Version
-###:
+0.4.7:
Abstracted powf() to an osdepend wrapper. (Needed for Windows.)
Fixed a @ceil bug, for some C math libraries.
+ Improved the profiling system in several ways.
+ Fixed a bug in glkop.c dispatching, to do with optional array
+ arguments.
0.4.6:
Added floating-point math feature.
break;
case op_streamchar:
- profile_in(2, FALSE);
+ profile_in(0xE0000001, stackptr, FALSE);
value = inst[0].value & 0xFF;
(*stream_char_handler)(value);
- profile_out();
+ profile_out(stackptr);
break;
case op_streamunichar:
- profile_in(2, FALSE);
+ profile_in(0xE0000002, stackptr, FALSE);
value = inst[0].value;
(*stream_unichar_handler)(value);
- profile_out();
+ profile_out(stackptr);
break;
case op_streamnum:
- profile_in(2, FALSE);
+ profile_in(0xE0000003, stackptr, FALSE);
vals0 = inst[0].value;
stream_num(vals0, FALSE, 0);
- profile_out();
+ profile_out(stackptr);
break;
case op_streamstr:
- profile_in(2, FALSE);
+ profile_in(0xE0000004, stackptr, FALSE);
stream_string(inst[0].value, 0, 0);
- profile_out();
+ profile_out(stackptr);
break;
default:
break;
case op_glk:
- profile_in(1, FALSE);
+ profile_in(0xF0000000+inst[0].value, stackptr, FALSE);
value = inst[1].value;
arglist = pop_arguments(value, 0);
val0 = perform_glk(inst[0].value, value, arglist);
store_operand(inst[2].desttype, inst[2].value, val0);
- profile_out();
+ profile_out(stackptr);
break;
case op_random:
accelfunc = accel_get_func(addr);
if (accelfunc) {
- profile_in(addr, TRUE);
+ profile_in(addr, stackptr, TRUE);
val = accelfunc(argc, argv);
- profile_out();
+ profile_out(stackptr);
pop_callstub(val);
return;
}
- profile_in(addr, FALSE);
+ profile_in(addr, stackptr, FALSE);
/* Check the Glulx type identifier byte. */
functype = Mem1(addr);
*/
void leave_function()
{
+ profile_out(stackptr);
stackptr = frameptr;
- profile_out();
}
/* push_callstub():
return 0x00030102; /* Glulx spec version 3.1.2 */
case gestulx_TerpVersion:
- return 0x00000406; /* Glulxe version 0.4.6 */
+ return 0x00000407; /* Glulxe version 0.4.7 */
case gestulx_ResizeMem:
#ifdef FIXED_MEMSIZE
static int num_classes = 0;
classtable_t **classes = NULL;
+static glui32 find_id_for_stream(strid_t str);
+
static classtable_t *new_classtable(glui32 firstid);
static void *classes_get(int classid, glui32 objid);
static classref_t *classes_put(int classid, void *obj);
directly -- instead of bothering with the whole prototype
mess. */
+ case 0x0047: /* stream_set_current */
+ if (numargs != 1)
+ goto WrongArgNum;
+ glk_stream_set_current(find_stream_by_id(arglist[0]));
+ break;
+ case 0x0048: /* stream_get_current */
+ if (numargs != 0)
+ goto WrongArgNum;
+ retval = find_id_for_stream(glk_stream_get_current());
+ break;
case 0x0080: /* put_char */
if (numargs != 1)
goto WrongArgNum;
goto WrongArgNum;
retval = glk_char_to_upper(arglist[0] & 0xFF);
break;
+ case 0x0128: /* put_char_uni */
+ if (numargs != 1)
+ goto WrongArgNum;
+ glk_put_char_uni(arglist[0]);
+ break;
+ case 0x012B: /* put_char_stream_uni */
+ if (numargs != 2)
+ goto WrongArgNum;
+ glk_put_char_stream_uni(find_stream_by_id(arglist[0]), arglist[1]);
+ break;
WrongArgNum:
fatal_error("Wrong number of arguments to Glk function.");
/* Go through the full dispatcher prototype foo. */
char *proto, *cx;
dispatch_splot_t splot;
- int argnum;
+ int argnum, argnum2;
/* Grab the string. */
proto = gidispatch_prototype(funcnum);
gidispatch_call(funcnum, argnum, splot.garglist);
/* Phase 3. */
- argnum = 0;
+ argnum2 = 0;
cx = proto;
- unparse_glk_args(&splot, &cx, 0, &argnum, 0, 0);
+ unparse_glk_args(&splot, &cx, 0, &argnum2, 0, 0);
+ if (argnum != argnum2)
+ fatal_error("Argument counts did not match.");
break;
}
}
else {
cx++;
+ if (isarray)
+ ix++;
}
}
}
}
else {
cx++;
+ if (isarray)
+ ix++;
}
}
}
return classes_get(1, objid);
}
+/* find_id_for_stream():
+ The converse of find_stream_by_id().
+ This is only needed in this file, so it's static.
+*/
+static glui32 find_id_for_stream(strid_t str)
+{
+ gidispatch_rock_t objrock;
+
+ if (!str)
+ return 0;
+
+ objrock = gidispatch_get_objrock(str, 1);
+ return ((classref_t *)objrock.ptr)->id;
+}
+
/* Build a hash table to hold a set of Glk objects. */
static classtable_t *new_classtable(glui32 firstid)
{
extern strid_t find_stream_by_id(glui32 objid);
/* profile.c */
+extern void setup_profile(strid_t stream, char *filename);
extern int init_profile(void);
#if VM_PROFILING
extern glui32 profile_opcount;
#define profile_tick() (profile_opcount++)
-extern void profile_in(glui32 addr, int accel);
-extern void profile_out(void);
+extern void profile_in(glui32 addr, glui32 stackuse, int accel);
+extern void profile_out(glui32 stackuse);
extern void profile_fail(char *reason);
extern void profile_quit(void);
#else /* VM_PROFILING */
-#define profile_tick() (0)
-#define profile_in(addr, accel) (0)
-#define profile_out() (0)
-#define profile_fail(reason) (0)
-#define profile_quit() (0)
+#define profile_tick() (0)
+#define profile_in(addr, stackuse, accel) (0)
+#define profile_out(stackuse) (0)
+#define profile_fail(reason) (0)
+#define profile_quit() (0)
#endif /* VM_PROFILING */
/* accel.c */
*/
int heap_get_summary(glui32 *valcount, glui32 **summary)
{
- glui32 *arr, len, pos, lx;
+ glui32 *arr, len, pos;
heapblock_t *blo;
*valcount = 0;
compiler (or the assembly output), and use that to figure out the
names of all the functions that were profiled.
+You can also generate profiling output in the same form as dumbfrotz's
+Z-machine profiling output. (If that happens to be what you want.) Use
+the --dumbfrotz argument.
+
Using this script is currently a nuisance. The requirements:
- You must compile Glulxe with profiling (the VM_PROFILING compile-time
option).
- (If you want function names) you should compile your Inform 6 source
using the -k switch. This generates a "gameinfo.dbg" file.
-- Run Glulxe, play some of the game, and quit. This generates a data
- file called "profile-raw".
+- Run Glulxe, using the "--profile profile-raw" option. Play some of
+ the game, and quit. This generates a data file called "profile-raw".
- Run this script, giving gameinfo.dbg and profile-raw as arguments.
+ (You can provide dispatch_dump.xml as an optional third argument.
+ This file gives the names of Glk functions; it is available from
+ https://github.com/erkyrath/glk-dev/tree/master/dispatch_dump .)
To sum up, in command-line form:
% inform -G -k game.inf
-% glulxe game.ulx
-% python profile-analyze.py profile-raw gameinfo.dbg
+% glulxe --profile profile-raw game.ulx
+% python profile-analyze.py profile-raw gameinfo.dbg dispatch_dump.xml
You can also use the assembly output of the Inform compiler, which you
get with the -a switch. Save the output and use it instead of the debug
file:
% inform -G -a game.inf > game.asm
-% glulxe game.ulx
-% python profile-analyze.py profile-raw game.asm
+% glulxe --profile profile-raw game.ulx
+% python profile-analyze.py profile-raw game.asm dispatch_dump.xml
The limitations:
often the most costly ones. (Therefore, you'll almost certainly want
to use -k.)
+If you leave off the "dispatch_dump.xml" argument, everything will
+still work, but @glk function entries will be listed by number rather
+than by name.
+
You can explore the profiling data in more detail by running the script
interactively:
-% python -i profile-analyze.py profile-raw game.asm
+% python -i profile-analyze.py profile-raw game.asm dispatch_dump.xml
After it runs, you'll be left at a Python prompt. The environment
will contain mappings called "functions" (mapping addresses to
function).
self_ops=INT: The number of opcodes executed during all calls to
the function, excluding time spent in subcalls.
+ max_depth=INT: The deepest this function has been nested on the
+ stack, during any call.
+ max_stack_use=INT: The greatest number of words on the stack, during
+ any call. (This is measured when the function returns, so it may
+ not capture the peak stack usage. If a function never returns, e.g.
+ Main__(), then this value is approximate.)
(The self_time is the "cost" used for the original listing.)
Note that if a function does not make any function calls, total_time
will be the same as self_time (and total_ops the same as self_ops).
-Two special function entries may be included. The function with address
-"1" (which is not a legal Glulx function address) represents time spent
-in @glk opcode calls. This will typically have a large self_time,
-because it includes all the time spent waiting for input.
+Some of the function entries refer to special interpreter operations.
+(These have high addresses, outside the range of normal game files.)
+Functions with addresses in the 0xE0000000 range are the interpreter's
+output opcodes: @streamchar, @streamunichar, @streamnum, @streamstr.
-The function with address "2" represents the time spent printing string
-data (the @streamchar, @streamunichar, @streamnum, and @streamstr
-opcodes).
+Functions with addresses in the 0xF0000000 range are @glk opcode calls.
+The number in the lower bits specifies which Glk function was called.
+You will always see a large self_time for function 0xF00000C0; this
+represents all the time spent waiting for input in glk_select().
-(Both "1" and "2" represent time spent in the Glk library, but they
-get there by different code paths.)
+(Both the 0xE0000000 and 0xF0000000 entries represent time spent in the
+Glk library, but they get there by different code paths.)
-The function with the lowest address (ignoring "1" and "2") is the
-top-level Main__() function generated by the compiler. Its total_time
-is the running time of the entire program.
+The function with the lowest address is the top-level Main__()
+function generated by the compiler. Its total_time is the running time
+of the entire program; its total_ops is the number of opcodes executed
+by the entire program; its max_depth is zero.
"""
import xml.sax
from struct import unpack
+dumb_frotz_mode = False
+
+if ('--dumbfrotz' in sys.argv):
+ sys.argv.remove('--dumbfrotz')
+ dumb_frotz_mode = True
+
if (len(sys.argv) < 2):
- print "Usage: profile-analyze.py profile-raw [ gameinfo.dbg | game.asm ]"
+ print "Usage: profile-analyze.py [--dumbfrotz] profile-raw [ gameinfo.dbg | game.asm ] [ dispatch_dump.xml ]"
sys.exit(1)
profile_raw = sys.argv[1]
print 'File not readable:', game_asm
sys.exit(1)
+dispatch_dump = None
+if (len(sys.argv) >= 4):
+ dispatch_dump = sys.argv[3]
+ if (not os.path.exists(dispatch_dump)):
+ print 'File not readable:', dispatch_dump
+ sys.exit(1)
+
special_functions = {
- 1: 'glk', 2: 'streamout'
+ 0xE0000001: 'streamchar',
+ 0xE0000002: 'streamunichar',
+ 0xE0000003: 'streamnum',
+ 0xE0000004: 'streamstr',
}
-max_special_functions = max(special_functions.keys())
+
+glk_functions = {}
functions = None
sourcemap = None
self.addr = addr
self.hexaddr = hexaddr
val = special_functions.get(addr)
- if (val is None):
+ if (addr >= 0xF0000000):
+ name = glk_functions.get(addr-0xF0000000)
+ if (not name):
+ name = hex(addr-0xF0000000)[2:]
+ name = '$' + name.replace('L', '')
+ self.name = '<@glk_' + name + '>'
+ self.special = True
+ elif (val is None):
self.name = '<???>'
self.special = False
else:
self.total_time = float(attrs['total_time'])
self.self_ops = int(attrs['self_ops'])
self.self_time = float(attrs['self_time'])
+ if (attrs.has_key('max_depth')):
+ self.max_depth = int(attrs['max_depth'])
+ if (attrs.has_key('max_stack_use')):
+ self.max_stack_use = int(attrs['max_stack_use'])
def __repr__(self):
return '<Function $' + self.hexaddr + ' ' + repr(self.name) + '>'
print ' %.6f sec (%d ops) spent executing' % (self.self_time, self.self_ops)
print ' %.6f sec (%d ops) including child calls' % (self.total_time, self.total_ops)
+ def dump_dumbfrotz_style(self):
+ percent1 = ' '
+ percent2 = ' '
+ pc1 = int(100*(float(self.self_ops)/float(ops_executed)))
+ pc2 = int(100*(float(self.total_ops)/float(ops_executed)))
+ if (pc1 > 0):
+ percent1 = "%3d%%" % pc1
+ if (pc2 > 0):
+ percent2 = "%3d%%" % pc2
+ print '%-36s %s %-10lu %s %-10lu %-10lu %-4d' % (self.name, percent1, self.self_ops, percent2, self.total_ops, self.call_count, self.max_depth)
+
+class DispatchDumpHandler(xml.sax.handler.ContentHandler):
+ def startElement(self, name, attrs):
+ if (name == 'function'):
+ addr = int(attrs['id'])
+ glk_functions[addr] = str(attrs['name'])
+
class ProfileRawHandler(xml.sax.handler.ContentHandler):
def startElement(self, name, attrs):
global functions
dat = fl.read(1)
num = unpack('>B', dat)[0]
name = self.read_string(fl)
- self.arrays[num] = name
+ self.globals[num] = name
def read_array_rec(self, fl):
dat = fl.read(2)
# Begin the work
+if (dispatch_dump):
+ xml.sax.parse(dispatch_dump, DispatchDumpHandler())
+
xml.sax.parse(profile_raw, ProfileRawHandler())
source_start = min([ func.addr for func in functions.values()
if not func.special ])
-print 'Code segment begins at', hex(source_start)
-print len(functions), 'called functions found in', profile_raw
+if (not dumb_frotz_mode):
+ print 'Code segment begins at', hex(source_start)
+ print len(functions), 'called functions found in', profile_raw
if (game_asm):
fl = open(game_asm, 'rb')
val = fl.read(2)
fl.close()
- if (val == '\xde\xbf'):
+ if (not val):
+ pass
+ elif (val == '\xde\xbf'):
fl = open(game_asm, 'rb')
debugfile = DebugFile(fl)
fl.close()
func.name = funcname
func.linenum = linenum
- if (badls):
- print len(badls), 'functions from', profile_raw, 'did not appear in asm (veneer functions)'
+ if (not dumb_frotz_mode):
+ if (badls):
+ print len(badls), 'functions from', profile_raw, 'did not appear in asm (veneer functions)'
function_names = {}
for func in functions.values():
if (sourcemap):
uncalled_funcs = [ funcname for (addr, (linenum, funcname)) in sourcemap.items() if (addr+source_start) not in functions ]
- print len(uncalled_funcs), 'functions found in', game_asm, 'were never called'
-
-print 'Functions that consumed the most time (excluding children):'
-ls = functions.values()
-ls.sort(lambda x1, x2: cmp(x2.self_time, x1.self_time))
-for func in ls[:10]:
- func.dump()
-
+ if (not dumb_frotz_mode):
+ print len(uncalled_funcs), 'functions found in', game_asm, 'were never called'
+
+if (dumb_frotz_mode):
+ ls = functions.values()
+ ls.sort(lambda x1, x2: cmp(x2.total_ops, x1.total_ops))
+ ops_executed = 0
+ routine_calls = 0
+ max_stack_use = max([func.max_stack_use for func in ls])
+ for func in ls:
+ if (func.total_ops > ops_executed):
+ ops_executed = func.total_ops
+ routine_calls = routine_calls + func.call_count
+ print 'Total opcodes: %lu' % ops_executed
+ print 'Total routine calls: %lu' % routine_calls
+ print 'Max. stack usage: %li' % max_stack_use
+ print ''
+ print '%-35s %-10s %-10s %-10s %-4s' % ('Routine', 'Ops', 'Ops(+Subs)', 'Calls', 'Nest')
+ for func in ls:
+ func.dump_dumbfrotz_style()
+else:
+ print 'Functions that consumed the most time (excluding children):'
+ ls = functions.values()
+ ls.sort(lambda x1, x2: cmp(x2.self_time, x1.self_time))
+ for func in ls[:10]:
+ func.dump()
@restoreundo, or @throw will kill the interpreter.
On a normal VM exit (end of top-level routine or @quit), the profiler
-writes out a data file called "profile-raw". This is an XML file of
-the form
+writes out a data file, using the filename you provided with the
+"--profile" option. Note that if the VM exits via glk_exit(), or is
+interrupted, the data file will be created (empty) but never filled in.
+
+The data file is an XML file of the form
<profile>
<function ... />
function).
self_ops=INT: The number of opcodes executed during all calls to
the function, excluding time spent in subcalls.
+ max_depth=INT: The deepest this function has been nested on the
+ stack, during any call.
+ max_stack_use=INT: The greatest number of words on the stack, during
+ any call. (This is measured when the function returns, so it may
+ not capture the peak stack usage. If a function never returns, e.g.
+ Main__(), then this value is approximate.)
Note that if a function does not make any function calls, total_time
will be the same as self_time (and total_ops the same as self_ops).
-Two special function entries may be included. The function with address
-"1" (which is not a legal Glulx function address) represents time spent
-in @glk opcode calls. This will typically have a large self_time,
-because it includes all the time spent waiting for input.
+Some of the function entries refer to special interpreter operations.
+(These have high addresses, outside the range of normal game files.)
+Functions with addresses in the 0xE0000000 range are the interpreter's
+output opcodes: @streamchar, @streamunichar, @streamnum, @streamstr.
-The function with address "2" represents the time spent printing string
-data (the @streamchar, @streamunichar, @streamnum, and @streamstr
-opcodes).
+Functions with addresses in the 0xF0000000 range are @glk opcode calls.
+The number in the lower bits specifies which Glk function was called.
+You will always see a large self_time for function 0xF00000C0; this
+represents all the time spent waiting for input in glk_select().
-(Both "1" and "2" represent time spent in the Glk library, but they
-get there by different code paths.)
+(Both the 0xE0000000 and 0xF0000000 entries represent time spent in the
+Glk library, but they get there by different code paths.)
-The function with the lowest address (ignoring "1" and "2") is the
-top-level Main__() function generated by the compiler. Its total_time
-is the running time of the entire program.
+The function with the lowest address is the top-level Main__()
+function generated by the compiler. Its total_time is the running time
+of the entire program; its total_ops is the number of opcodes executed
+by the entire program; its max_depth is zero.
*/
#include <string.h>
#include <sys/time.h>
+/* Set if the --profile switch is used. */
+static int profiling_active = FALSE;
+static char *profiling_filename = NULL;
+static strid_t profiling_stream = NULL;
+
typedef struct function_struct {
glui32 addr;
glui32 entry_depth;
struct timeval entry_start_time;
glui32 entry_start_op;
+ glui32 max_depth;
+ glui32 max_stack_use;
struct timeval total_time;
glui32 total_ops;
struct timeval self_time;
typedef struct frame_struct {
struct frame_struct *parent;
function_t *func;
+ glui32 depth;
struct timeval entry_time;
glui32 entry_op;
static function_t **functions = NULL;
static frame_t *current_frame = NULL;
+/* This counter is globally visible, because the profile_tick() macro
+ increments it. */
glui32 profile_opcount = 0;
+/* This is called from the setup code -- glkunix_startup_code(), for the
+ Unix version. If called, the interpreter will keep profiling information,
+ and write it out at shutdown time. If this is not called, the interpreter
+ will skip all the profiling code. (Although it won't be quite as fast
+ as if the VM_PROFILING symbol were compiled out entirely.)
+
+ The arguments are a little tricky, because I developed this on Unix,
+ but I want it to remain accessible on all platforms. Pass a writable
+ stream object as the first argument; at game-shutdown time, the terp
+ will write the profiling data to this object and then close it.
+
+ However, if it's not convenient to open a stream in the startup code,
+ you can simply pass a filename as the second argument. This filename
+ will be opened according to the usual Glk data file rules, which means
+ it may wind up in a sandboxed data directory. The filename should not
+ contain slashes or other pathname separators.
+
+ If you pass NULL for both arguments, a file called "profile-raw" will
+ be written.
+*/
+void setup_profile(strid_t stream, char *filename)
+{
+ profiling_active = TRUE;
+
+ if (stream)
+ profiling_stream = stream;
+ else if (filename)
+ profiling_filename = filename;
+ else
+ profiling_filename = "profile-raw";
+}
+
int init_profile()
{
int bucknum;
+ if (!profiling_active)
+ return TRUE;
+
functions = (function_t **)glulx_malloc(FUNC_HASH_SIZE
* sizeof(function_t *));
if (!functions)
func->total_ops = 0;
timerclear(&func->self_time);
func->self_ops = 0;
+ func->max_depth = 0;
+ func->max_stack_use = 0;
}
return func;
return buf;
}
-void profile_in(glui32 addr, int accel)
+void profile_in(glui32 addr, glui32 stackuse, int accel)
{
frame_t *fra;
function_t *func;
struct timeval now;
+ if (!profiling_active)
+ return;
+
/* printf("### IN: %lx%s\n", addr, (accel?" accel":"")); */
gettimeofday(&now, NULL);
}
func->entry_depth += 1;
+ if (func->max_stack_use < stackuse)
+ func->max_stack_use = stackuse;
+
fra = (frame_t *)glulx_malloc(sizeof(frame_t));
if (!fra)
fatal_error("Profiler: cannot malloc frame.");
fra->parent = current_frame;
current_frame = fra;
+ if (fra->parent)
+ fra->depth = fra->parent->depth + 1;
fra->func = func;
fra->entry_time = now;
fra->entry_op = profile_opcount;
fra->children_ops = 0;
}
-void profile_out()
+void profile_out(glui32 stackuse)
{
frame_t *fra;
function_t *func;
struct timeval now, runtime;
glui32 runops;
+ if (!profiling_active)
+ return;
+
/* printf("### OUT\n"); */
if (!current_frame)
func->self_ops += runops;
func->self_ops -= fra->children_ops;
+ if (func->max_depth < fra->depth)
+ func->max_depth = fra->depth;
+ if (func->max_stack_use < stackuse)
+ func->max_stack_use = stackuse;
+
if (fra->parent) {
timeradd(&runtime, &fra->parent->children_time, &fra->parent->children_time);
fra->parent->children_ops += runops;
void profile_fail(char *reason)
{
+ if (!profiling_active)
+ return;
+
fatal_error_2("Profiler: unable to handle operation", reason);
}
int bucknum;
function_t *func;
char linebuf[512];
- frefid_t profref;
strid_t profstr;
+ if (!profiling_active)
+ return;
+
while (current_frame) {
- profile_out();
+ profile_out(0);
}
- profref = glk_fileref_create_by_name(fileusage_BinaryMode|fileusage_Data, "profile-raw", 0);
- if (!profref)
- fatal_error("Profiler: unable to create profile-raw file");
+ if (profiling_stream) {
+ profstr = profiling_stream;
+ }
+ else if (profiling_filename) {
+ frefid_t profref = glk_fileref_create_by_name(fileusage_BinaryMode|fileusage_Data, profiling_filename, 0);
+ if (!profref)
+ fatal_error_2("Profiler: unable to create profile output fileref", profiling_filename);
- profstr = glk_stream_open_file(profref, filemode_Write, 0);
+ profstr = glk_stream_open_file(profref, filemode_Write, 0);
+ }
+ else {
+ fatal_error("Profiler: no profile output handle!");
+ }
glk_put_string_stream(profstr, "<profile>\n");
func->self_ops,
timeprint(&func->self_time, self_buf));
### */
- sprintf(linebuf, " <function addr=\"%lx\" call_count=\"%ld\" accel_count=\"%ld\" total_ops=\"%ld\" total_time=\"%s\" self_ops=\"%ld\" self_time=\"%s\" />\n",
- func->addr, func->call_count, func->accel_count,
- func->total_ops,
+ sprintf(linebuf, " <function addr=\"%lx\" call_count=\"%ld\" accel_count=\"%ld\" total_ops=\"%ld\" total_time=\"%s\" self_ops=\"%ld\" self_time=\"%s\" max_depth=\"%ld\" max_stack_use=\"%ld\" />\n",
+ (unsigned long)func->addr, (long)func->call_count, (long)func->accel_count,
+ (long)func->total_ops,
timeprint(&func->total_time, total_buf),
- func->self_ops,
- timeprint(&func->self_time, self_buf));
+ (long)func->self_ops,
+ timeprint(&func->self_time, self_buf),
+ (long)func->max_depth, (long)func->max_stack_use);
glk_put_string_stream(profstr, linebuf);
}
}
#else /* VM_PROFILING */
+void setup_profile(strid_t stream, char *filename)
+{
+ /* Profiling is not compiled in. Do nothing. */
+}
+
int init_profile()
{
/* Profiling is not compiled in. Do nothing. */
#include "glk.h"
#include "glulxe.h"
-#include "opcodes.h"
#define serop_KeyIndirect (0x01)
#define serop_ZeroKeyTerminates (0x02)
static int undo_chain_num = 0;
unsigned char **undo_chain = NULL;
-static glui32 protect_pos = 0;
-static glui32 protect_len = 0;
-
static glui32 write_memstate(dest_t *dest);
static glui32 write_heapstate(dest_t *dest, int portable);
static glui32 write_stackstate(dest_t *dest, int portable);
static glui32 write_stackstate(dest_t *dest, int portable)
{
- glui32 res, pos;
- glui32 val, lx;
- unsigned char ch;
+ glui32 res;
+ glui32 lx;
glui32 lastframe;
/* If we're storing for the purpose of undo, we don't need to do any
static glui32 read_stackstate(dest_t *dest, glui32 chunklen, int portable)
{
- glui32 res, pos;
- unsigned char ch;
+ glui32 res;
glui32 frameend, frm, frm2, frm3, locpos, frlen, numlocals;
if (chunklen > stacksize)
{
int ix, len;
glui32 addr2;
- char *res, *cx;
+ char *res;
if (Mem1(addr) != 0xE0)
fatal_error("String argument to a Glk call must be unencoded.");
{
int ix, len;
glui32 addr2;
- glui32 *res, *cx;
+ glui32 *res;
if (Mem1(addr) != 0xE2)
fatal_error("Ustring argument to a Glk call must be unencoded.");
http://eblong.com/zarf/glulx/index.html
*/
+#include <string.h>
#include "glk.h"
#include "glulxe.h"
#include "glkstart.h" /* This comes with the Glk library. */
-#include <string.h>
-/* The only command-line argument is the filename. */
+/* The only command-line argument is the filename. And the profiling switch,
+ if that's compiled in. The only *two* command-line arguments are...
+*/
glkunix_argumentlist_t glkunix_arguments[] = {
+
+#if VM_PROFILING
+ { "--profile", glkunix_arg_ValueFollows, "Generate profiling information to a file." },
+#endif /* VM_PROFILING */
+
{ "", glkunix_arg_ValueFollows, "filename: The game file to load." },
+
{ NULL, glkunix_arg_End, NULL }
};
{
/* It turns out to be more convenient if we return TRUE from here, even
when an error occurs, and display an error in glk_main(). */
- char *cx;
+ int ix;
+ char *filename = NULL;
unsigned char buf[12];
int res;
#ifdef GARGLK
- garglk_set_program_name("Glulxe 0.4.6");
- garglk_set_program_info("Glulxe 0.4.6 by Andrew Plotkin");
+ char *cx;
+ garglk_set_program_name("Glulxe 0.4.7");
+ garglk_set_program_info("Glulxe 0.4.7 by Andrew Plotkin");
#endif
- if (data->argc <= 1) {
+ /* Parse out the arguments. They've already been checked for validity,
+ and the library-specific ones stripped out.
+ As usual for Unix, the zeroth argument is the executable name. */
+ for (ix=1; ix<data->argc; ix++) {
+
+#if VM_PROFILING
+ if (!strcmp(data->argv[ix], "--profile")) {
+ ix++;
+ if (ix<data->argc) {
+ strid_t profstr = glkunix_stream_open_pathname_gen(data->argv[ix], TRUE, FALSE, 1);
+ if (!profstr) {
+ init_err = "Unable to open profile output file.";
+ init_err2 = data->argv[ix];
+ return TRUE;
+ }
+ setup_profile(profstr, NULL);
+ }
+ continue;
+ }
+#endif /* VM_PROFILING */
+
+ if (filename) {
+ init_err = "You must supply exactly one game file.";
+ return TRUE;
+ }
+ filename = data->argv[ix];
+ }
+
+ if (!filename) {
init_err = "You must supply the name of a game file.";
#ifdef GARGLK
return TRUE; /* Hack! but I want error message in glk window */
#endif
return FALSE;
}
- cx = data->argv[1];
- gamefile = glkunix_stream_open_pathname(cx, FALSE, 1);
+ gamefile = glkunix_stream_open_pathname(filename, FALSE, 1);
if (!gamefile) {
init_err = "The game file could not be opened.";
- init_err2 = cx;
+ init_err2 = filename;
return TRUE;
}
#ifdef GARGLK
- cx = strrchr(data->argv[1], '/');
- if (!cx) cx = strrchr(data->argv[1], '\\');
- garglk_set_story_name(cx ? cx + 1 : data->argv[1]);
+ cx = strrchr(filename, '/');
+ if (!cx) cx = strrchr(filename, '\\');
+ garglk_set_story_name(cx ? cx + 1 : filename);
#endif
/* Now we have to check to see if it's a Blorb file. */
style.c style.h \
timer.c timer.h \
window.c window.h
-libchimara_la_CPPFLAGS = \
+libchimara_la_CPPFLAGS = $(AM_CPPFLAGS) \
-DG_LOG_DOMAIN=\"Chimara\" \
-DLOCALEDIR=\""$(datadir)/locale"\" \
-DPLUGINDIR=\""$(pluginpath)"\" \
endif
GITIGNOREFILES = Chimara-1.0.gir Chimara-1.0.typelib
-# Currently, install the Vala VAPI file statically - generation is broken?
-
vapidir = $(datadir)/vala/vapi
dist_vapi_DATA = chimara.vapi
+if BUILDING_VAPI
+chimara.vapi: $(INTROSPECTION_GIRS)
+ $(AM_V_GEN)$(VAPIGEN) --library=chimara --pkg gtk+-2.0 Chimara-1.0.gir && \
+ touch $@
+endif
+
+MAINTAINERCLEANFILES = chimara.vapi
-include $(top_srcdir)/git.mk
PROP_SPACING,
PROP_PROGRAM_NAME,
PROP_PROGRAM_INFO,
- PROP_STORY_NAME
+ PROP_STORY_NAME,
+ PROP_RUNNING
};
enum {
static void
chimara_glk_init(ChimaraGlk *self)
{
+ chimara_init(); /* This is a library entry point */
+
gtk_widget_set_has_window(GTK_WIDGET(self), FALSE);
ChimaraGlkPrivate *priv = CHIMARA_GLK_PRIVATE(self);
case PROP_STORY_NAME:
g_value_set_string(value, priv->story_name);
break;
+ case PROP_RUNNING:
+ g_value_set_boolean(value, priv->running);
+ break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
}
NULL,
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS) );
+ /**
+ * ChimaraGlk:running:
+ *
+ * Whether this Glk widget is currently running a game or not.
+ */
+ g_object_class_install_property(object_class, PROP_RUNNING,
+ g_param_spec_boolean("running", _("Running"),
+ _("Whether there is a program currently running"),
+ FALSE,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS) );
+
/* Private data */
g_type_class_add_private(klass, sizeof(ChimaraGlkPrivate));
}
*
* This structure contains no public members.
*/
-typedef struct _ChimaraGlk {
+typedef struct {
GtkContainer parent_instance;
/*< public >*/
} ChimaraGlk;
-typedef struct _ChimaraGlkClass {
+typedef struct {
GtkContainerClass parent_class;
/* Signals */
void(* stopped) (ChimaraGlk *self);
static void
chimara_if_init(ChimaraIF *self)
{
+ chimara_init(); /* This is a library entry point */
+
CHIMARA_IF_USE_PRIVATE(self, priv);
priv->preferred_interpreter[CHIMARA_IF_FORMAT_Z5] = CHIMARA_IF_INTERPRETER_FROTZ;
priv->preferred_interpreter[CHIMARA_IF_FORMAT_Z6] = CHIMARA_IF_INTERPRETER_NITFOL;
*
* Constants representing all game formats supported by the Chimara system.
*/
-typedef enum _ChimaraIFFormat {
+typedef enum {
/*< private >*/
CHIMARA_IF_FORMAT_NONE = -1,
/*< public >*/
*
* Constants representing the available interpreter plugins.
*/
-typedef enum _ChimaraIFInterpreter {
+typedef enum {
/*< private >*/
CHIMARA_IF_INTERPRETER_NONE = -1,
/*< public >*/
* Allowed values for the #ChimaraIF:interpreter-number property. All trademarks
* are the property of their respective owners.
*/
-typedef enum _ChimaraIFZmachineVersion {
+typedef enum {
CHIMARA_IF_ZMACHINE_DEFAULT = 0,
CHIMARA_IF_ZMACHINE_DECSYSTEM_20,
CHIMARA_IF_ZMACHINE_APPLE_IIE,
*
* This structure contains no public members.
*/
-typedef struct _ChimaraIF {
+typedef struct {
ChimaraGlk parent_instance;
/*< public >*/
} ChimaraIF;
-typedef struct _ChimaraIFClass {
+typedef struct {
ChimaraGlkClass parent_class;
/* Signals */
void(* command) (ChimaraIF *self, gchar *input, gchar *response);
+++ /dev/null
-/* chimara.vapi generated by vapigen, do not modify. */
-
-[CCode (cprefix = "Chimara", lower_case_cprefix = "chimara_", gir_namespace = "Chimara", gir_version = "1.0")]
-namespace Chimara {
- [CCode (cheader_filename = "libchimara/chimara-glk.h,libchimara/chimara-if.h")]
- public class Glk : Gtk.Container, Atk.Implementor, Gtk.Buildable {
- [CCode (type = "GtkWidget*", has_construct_function = false)]
- public Glk ();
- public void feed_char_input (uint32 keyval);
- public void feed_line_input (string text);
- public bool get_interactive ();
- public bool get_protect ();
- public bool get_running ();
- public uint get_spacing ();
- public unowned Gtk.TextTag get_tag (Chimara.GlkWindowType window, string name);
- public unowned string[] get_tag_names (out uint num_tags);
- public bool is_char_input_pending ();
- public bool is_line_input_pending ();
- public bool run (string plugin, int argc, string argv) throws GLib.Error;
- public bool set_css_from_file (string filename) throws GLib.Error;
- public void set_css_from_string (string css);
- public void set_css_to_default ();
- public void set_interactive (bool interactive);
- public void set_protect (bool protect);
- public void set_resource_load_callback (owned Chimara.ResourceLoadFunc func);
- public void set_spacing (uint spacing);
- public void stop ();
- public void update_style ();
- public void wait ();
- public bool interactive { get; set construct; }
- [NoAccessorMethod]
- public string program_info { get; }
- [NoAccessorMethod]
- public string program_name { get; }
- public bool protect { get; set construct; }
- public uint spacing { get; set construct; }
- [NoAccessorMethod]
- public string story_name { get; }
- public signal void char_input (uint object, uint p0);
- public signal void iliad_screen_update (bool object);
- public signal void line_input (uint object, string p0);
- public signal void started ();
- public signal void stopped ();
- public signal void text_buffer_output (uint object, string p0);
- public signal void waiting ();
- }
- [CCode (cheader_filename = "libchimara/chimara-glk.h,libchimara/chimara-if.h")]
- public class IF : Chimara.Glk, Atk.Implementor, Gtk.Buildable {
- [CCode (type = "GtkWidget*", has_construct_function = false)]
- public IF ();
- public Chimara.IFFormat get_format ();
- public Chimara.IFInterpreter get_interpreter ();
- public Chimara.IFInterpreter get_preferred_interpreter (Chimara.IFFormat format);
- public bool run_game (string gamefile) throws GLib.Error;
- public void set_preferred_interpreter (Chimara.IFFormat format, Chimara.IFInterpreter interpreter);
- [NoAccessorMethod]
- public bool expand_abbreviations { get; set construct; }
- [NoAccessorMethod]
- public string graphics_file { get; set construct; }
- [NoAccessorMethod]
- public bool ignore_errors { get; set construct; }
- [NoAccessorMethod]
- public uint interpreter_number { get; set construct; }
- [NoAccessorMethod]
- public bool piracy_mode { get; set construct; }
- [NoAccessorMethod]
- public int random_seed { get; set; }
- [NoAccessorMethod]
- public bool random_seed_set { get; set construct; }
- [NoAccessorMethod]
- public bool tandy_bit { get; set construct; }
- [NoAccessorMethod]
- public bool typo_correction { get; set construct; }
- public signal void command (string object, string p0);
- }
- [CCode (cprefix = "CHIMARA_GLK_TEXT_", cheader_filename = "libchimara/chimara-glk.h,libchimara/chimara-if.h")]
- public enum GlkWindowType {
- BUFFER,
- GRID
- }
- [CCode (cprefix = "CHIMARA_IF_FORMAT_", cheader_filename = "libchimara/chimara-glk.h,libchimara/chimara-if.h")]
- public enum IFFormat {
- [CCode (cname = "CHIMARA_IF_FORMAT_Z5")]
- FORMAT_Z5,
- [CCode (cname = "CHIMARA_IF_FORMAT_Z6")]
- FORMAT_Z6,
- [CCode (cname = "CHIMARA_IF_FORMAT_Z8")]
- FORMAT_Z8,
- [CCode (cname = "CHIMARA_IF_FORMAT_Z_BLORB")]
- FORMAT_Z_BLORB,
- [CCode (cname = "CHIMARA_IF_FORMAT_GLULX")]
- FORMAT_GLULX,
- [CCode (cname = "CHIMARA_IF_FORMAT_GLULX_BLORB")]
- FORMAT_GLULX_BLORB
- }
- [CCode (cprefix = "CHIMARA_IF_INTERPRETER_", cheader_filename = "libchimara/chimara-glk.h,libchimara/chimara-if.h")]
- public enum IFInterpreter {
- [CCode (cname = "CHIMARA_IF_INTERPRETER_FROTZ")]
- INTERPRETER_FROTZ,
- [CCode (cname = "CHIMARA_IF_INTERPRETER_NITFOL")]
- INTERPRETER_NITFOL,
- [CCode (cname = "CHIMARA_IF_INTERPRETER_GLULXE")]
- INTERPRETER_GLULXE,
- [CCode (cname = "CHIMARA_IF_INTERPRETER_GIT")]
- INTERPRETER_GIT
- }
- [CCode (cprefix = "CHIMARA_IF_ZMACHINE_", cheader_filename = "libchimara/chimara-glk.h,libchimara/chimara-if.h")]
- public enum IFZmachineVersion {
- DEFAULT,
- DECSYSTEM_20,
- APPLE_IIE,
- MACINTOSH,
- AMIGA,
- ATARI_ST,
- IBM_PC,
- COMMODORE_128,
- COMMODORE_64,
- APPLE_IIC,
- APPLE_IIGS,
- TANDY_COLOR
- }
- [CCode (cprefix = "CHIMARA_RESOURCE_", cheader_filename = "libchimara/chimara-glk.h,libchimara/chimara-if.h")]
- public enum ResourceType {
- SOUND,
- IMAGE
- }
- [CCode (cprefix = "ERROR_", cheader_filename = "libchimara/chimara-glk.h,libchimara/chimara-if.h")]
- public errordomain Error {
- LOAD_MODULE_ERROR,
- NO_GLK_MAIN,
- PLUGIN_NOT_FOUND,
- PLUGIN_ALREADY_RUNNING,
- }
- [CCode (cheader_filename = "libchimara/chimara-glk.h,libchimara/chimara-if.h", instance_pos = 2.9)]
- public delegate string ResourceLoadFunc (Chimara.ResourceType usage, uint32 resnum);
- [CCode (cheader_filename = "libchimara/chimara-glk.h,libchimara/chimara-if.h")]
- public static GLib.Quark error_quark ();
-}
* A stream is opened with a particular file mode, see the
* <code>filemode_</code> constants below.
*
+ * <note><para>
+ * In the stdio library, using fopen(), %filemode_Write would be mode
+ * <code>"w"</code>; %filemode_Read would be mode <code>"r"</code>;
+ * %filemode_ReadWrite would be mode <code>"r+"</code>. Confusingly,
+ * %filemode_WriteAppend cannot be mode <code>"a"</code>, because the stdio
+ * spec says that when you open a file with mode <code>"a"</code>, then
+ * fseek() doesn't work. So we have to use mode <code>"r+"</code> for
+ * appending. Then we run into the <emphasis>other</emphasis> stdio problem,
+ * which is that <code>"r+"</code> never creates a new file. So
+ * %filemode_WriteAppend has to <emphasis>first</emphasis> open the file with
+ * <code>"a"</code>, close it, reopen with <code>"r+"</code>, and then
+ * fseek() to the end of the file. For %filemode_ReadWrite, the process is
+ * the same, except without the fseek() — we begin at the beginning of
+ * the file.
+ * </para></note>
+ * <note><para>
+ * We must also obey an obscure geas of ANSI C <code>"r+"</code> files: you
+ * can't switch from reading to writing without doing an fseek() in between.
+ * Switching from writing to reading has the same restriction, except that an
+ * fflush() also works.
+ * </para></note>
+ *
* For information on opening streams, see the discussion of each specific type
* of stream in <link linkend="chimara-The-Types-of-Streams">The Types of
* Streams</link>. Remember that it is always possible that opening a stream
/**
* SECTION:glk-stream-types
- * @short_description: Window, memory, and file streams
+ * @short_description: Window, memory, file, and resource streams
*
* <refsect2 id="chimara-Window-Streams"><title>Window Streams</title>
* <para>
* linkend="chimara-File-References">File References</link>.
* </para>
* </refsect2>
+ * <refsect2 id="chimara-Resource-Streams"><title>Resource Streams</title>
+ * <para>
+ * You can open a stream which reads from (but not writes to) a resource file.
+ *
+ * <note><para>
+ * Typically this is embedded in a Blorb file, as Blorb is the official
+ * resource-storage format of Glk. A Blorb file can contain images and sounds,
+ * but it can also contain raw data files, which are accessed by
+ * glk_stream_open_resource() and glk_stream_open_resource_uni(). A data file
+ * is identified by number, not by a filename. The Blorb usage field will be
+ * <code>'Data'</code>. The chunk type will be %giblorb_ID_TEXT for text
+ * resources, %giblorb_ID_BINA for binary resources.
+ * </para></note>
+ *
+ * <note><para>
+ * If the running program is not associated with a Blorb file, the library may
+ * look for data files as actual files instead. These would be named
+ * <filename>DATA1</filename>, <filename>DATA2</filename>, etc, with a suffix
+ * distinguishing text and binary files. See <quote>Other Resource
+ * Arrangements</quote> in the Blorb spec: <ulink
+ * url="http://eblong.com/zarf/blorb/"></ulink>
+ * </para></note>
+ * </para>
+ * </refsect2>
*/
/**
* SECTION:glk-sound-testing
* @short_description: Checking whether the library supports sound
*
- * Before calling Glk sound functions, you should use the %gestalt_Sound
- * selector. To test for additional capabilities, you can use the
- * %gestalt_SoundMusic, %gestalt_SoundVolume, and %gestalt_SoundNotify
- * selectors.
+ * Before calling Glk sound functions, you should use the %gestalt_Sound2
+ * selector.
+ *
+ * Earlier versions of the Glk spec defined separate selectors for various
+ * optional capabilities. This has proven to be an unnecessarily confusing
+ * strategy, and is no longer used. The %gestalt_Sound, %gestalt_SoundMusic,
+ * %gestalt_SoundVolume, and %gestalt_SoundNotify selectors still exist, but you
+ * should not need to test them; the %gestalt_Sound2 selector covers all of
+ * them.
*/
/**
*/
/**
- * GLK_MODULE_SOUND:
+ * GLK_MODULE_SOUND2:
*
* If you are writing a C program, there is an additional complication. A
* library which does not support sound may not implement the sound functions at
* even get compile-time errors.
*
* To avoid this, you can perform a preprocessor test for the existence of
- * %GLK_MODULE_SOUND. If this is defined, so are all the functions and constants
+ * %GLK_MODULE_SOUND2. If this is defined, so are all the functions and constants
* described in this section. If not, not.
- */
+ */
+
+/**
+ * GLK_MODULE_SOUND:
+ *
+ * You can perform a preprocessor test for the existence of %GLK_MODULE_SOUND.
+ * If this is defined, so are all the functions and constants described in this
+ * section. If not, not.
+ */
/**
* GLK_MODULE_HYPERLINKS:
* So the version number 78.2.11 would be encoded as 0x004E020B.
* </para></note>
*
- * The current Glk specification version is 0.7.2, so this selector will return
- * 0x00000702.
+ * The current Glk specification version is 0.7.4, so this selector will return
+ * 0x00000704.
*
* |[
* glui32 res;
* may implement both, neither, or only one.
*/
+/**
+ * gestalt_Sound2:
+ *
+ * You can test whether the library supports sound:
+ * |[
+ * glui32 res;
+ * res = glk_gestalt(gestalt_Sound2, 0);
+ * ]|
+ * This returns 1 if the overall suite of sound functions is available. This
+ * includes all the functions defined in <link
+ * linkend="chimara-glk-spec-sound">this chapter</link>. It also includes the
+ * capabilities described below under %gestalt_SoundMusic, %gestalt_SoundVolume,
+ * and %gestalt_SoundNotify.
+ */
+
/**
* gestalt_Sound:
*
- * You can test whether the library supports sound:
* |[
* glui32 res;
* res = glk_gestalt(gestalt_Sound, 0);
* glk_sound_load_hint().
*
* If this selector returns 0, you should not try to call these functions. They
- * may have no effect, or they may cause a run-time error.
+ * may have no effect, or they may cause a run-time error.
+ *
+ * This selector is guaranteed to return 1 if %gestalt_Sound2 does.
*/
/**
*
* You can test whether the library supports setting the volume of sound
* channels:
- * |[
- * glui32 res;
- * res = glk_gestalt(gestalt_SoundVolume, 0);
- * ]|
+ * |[ res = glk_gestalt(gestalt_SoundVolume, 0); ]|
* This selector returns 1 if the glk_schannel_set_volume() function works. If
- * it returns zero, glk_schannel_set_volume() has no effect.
+ * it returns zero, glk_schannel_set_volume() has no effect.
+ *
+ * This selector is guaranteed to return 1 if %gestalt_Sound2 does.
*/
/**
* gestalt_SoundNotify:
*
* You can test whether the library supports sound notification events:
- * |[
- * glui32 res;
- * res = glk_gestalt(gestalt_SoundNotify, 0);
- * ]|
+ * |[ res = glk_gestalt(gestalt_SoundNotify, 0); ]|
* This selector returns 1 if the library supports sound notification events. If
- * it returns zero, you will never get such events.
+ * it returns zero, you will never get such events.
+ *
+ * This selector is guaranteed to return 1 if %gestalt_Sound2 does.
*/
/**
* course, an ugly hack. It is a concession to the current state of the Glk
* libraries, some of which can handle AIFF but not MOD sounds.
* </para></note>
+ *
+ * This selector is guaranteed to return 1 if %gestalt_Sound2 does.
*/
/**
* </para></note>
*/
+/**
+ * gestalt_ResourceStream:
+ *
+ * |[
+ * res = glk_gestalt(gestalt_ResourceStream, 0);
+ * ]|
+ *
+ * This returns 1 if the glk_stream_open_resource() and
+ * glk_stream_open_resource_uni() functions are available. If it returns 0, you
+ * should not call them.
+ */
+
/**
* evtype_None:
*
/**
* evtype_SoundNotify:
*
+ * The completion of a sound being played in a sound channel.
+ *
* On platforms that support sound, you can request to receive an
* %evtype_SoundNotify event when a sound finishes playing. See <link
* linkend="chimara-Playing-Sounds">Playing Sounds</link>.
/**
* evtype_Hyperlink:
+ *
+ * The selection of a hyperlink in a window.
*
* On platforms that support hyperlinks, you can request to receive an
* %evtype_Hyperlink event when the player selects a link. See <link
* Events</link>.
*/
+/**
+ * evtype_VolumeNotify:
+ *
+ * The completion of a gradual volume change in a sound channel.
+ *
+ * On platforms that support sound, you can request to receive an
+ * %evtype_VolumeNotify event when a gradual volume change completes. See <link
+ * linkend="chimara-Playing-Sounds">Playing Sounds</link>.
+ */
+
/**
* event_t:
* @type: the event type
* linefeed-plus-carriage-return combinations; Latin-1 characters may be
* converted to native character codes. When reading a file in text mode, native
* line breaks will be converted back to newline (0x0A) characters, and native
- * character codes may be converted to Latin-1.
+ * character codes may be converted to Latin-1 or UTF-8.
*
* <note><para>
* Line breaks will always be converted; other conversions are more
* Resource usage constant representing an image file.
*/
+/**
+ * giblorb_ID_Data:
+ *
+ * Resource usage constant representing a data file.
+ */
+
/**
* giblorb_ID_Copyright:
*
* Resource usage constant representing any textual annotation that the user or
* writing program sees fit to include.
*/
-
+
+/**
+ * giblorb_ID_TEXT:
+ *
+ * Resource usage constant representing a text data file.
+ */
+
+/**
+ * giblorb_ID_BINA:
+ *
+ * Resource usage constant representing a binary data file.
+ */
+
/**
* giblorb_map_t:
*
extern GPrivate *glk_data_key;
-/* Internal function: create a fileref using the given parameters. */
+/* Internal function: create a fileref using the given parameters. If @basename
+is NULL, compute a basename from @filename. */
frefid_t
-fileref_new(gchar *filename, glui32 rock, glui32 usage, glui32 orig_filemode)
+fileref_new(char *filename, char *basename, glui32 rock, glui32 usage, glui32 orig_filemode)
{
g_return_val_if_fail(filename != NULL, NULL);
f->disprock = (*glk_data->register_obj)(f, gidisp_Class_Fileref);
f->filename = g_strdup(filename);
+ if(basename)
+ f->basename = g_strdup(basename);
+ else
+ f->basename = g_path_get_basename(filename);
f->usage = usage;
f->orig_filemode = orig_filemode;
fref->disprock.ptr = NULL;
}
- if(fref->filename)
- g_free(fref->filename);
+ g_free(fref->filename);
+ g_free(fref->basename);
fref->magic = MAGIC_FREE;
g_free(fref);
return NULL;
}
- frefid_t f = fileref_new(filename, rock, usage, filemode_Write);
+ /* Pass a basename of "" to ensure that this file can't be repurposed */
+ frefid_t f = fileref_new(filename, "", rock, usage, filemode_Write);
g_free(filename);
return f;
}
* value is valid before you use it.
* </para></note>
*
+ * The recommended file suffixes for files are <filename>.glkdata</filename> for
+ * %fileusage_Data, <filename>.glksave</filename> for %fileusage_SavedGame,
+ * <filename>.txt</filename> for %fileusage_Transcript and
+ * %fileusage_InputRecord.
+ *
* Returns: A new fileref, or #NULL if the fileref creation failed or the
* dialog was canceled.
*/
return NULL;
}
+ /* Set up a file filter with suggested extensions */
+ GtkFileFilter *filter = gtk_file_filter_new();
+ switch(usage & fileusage_TypeMask)
+ {
+ case fileusage_Data:
+ gtk_file_filter_set_name(filter, _("Data files (*.glkdata)"));
+ gtk_file_filter_add_pattern(filter, "*.glkdata");
+ break;
+ case fileusage_SavedGame:
+ gtk_file_filter_set_name(filter, _("Saved games (*.glksave)"));
+ gtk_file_filter_add_pattern(filter, "*.glksave");
+ break;
+ case fileusage_InputRecord:
+ gtk_file_filter_set_name(filter, _("Text files (*.txt)"));
+ gtk_file_filter_add_pattern(filter, "*.txt");
+ break;
+ case fileusage_Transcript:
+ gtk_file_filter_set_name(filter, _("Transcript files (*.txt)"));
+ gtk_file_filter_add_pattern(filter, "*.txt");
+ break;
+ default:
+ ILLEGAL_PARAM("Unknown file usage: %u", usage);
+ gdk_threads_leave();
+ return NULL;
+ }
+ gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(chooser), filter);
+
+ /* Add a "text mode" filter for text files */
+ if((usage & fileusage_TypeMask) == fileusage_InputRecord || (usage & fileusage_TypeMask) == fileusage_Transcript)
+ {
+ filter = gtk_file_filter_new();
+ gtk_file_filter_set_name(filter, _("All text files"));
+ gtk_file_filter_add_mime_type(filter, "text/plain");
+ gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(chooser), filter);
+ }
+
+ /* Add another non-restricted filter */
+ filter = gtk_file_filter_new();
+ gtk_file_filter_set_name(filter, _("All files"));
+ gtk_file_filter_add_pattern(filter, "*");
+ gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(chooser), filter);
+
if(glk_data->current_dir)
gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(chooser), glk_data->current_dir);
return NULL;
}
gchar *filename = gtk_file_chooser_get_filename( GTK_FILE_CHOOSER(chooser) );
- frefid_t f = fileref_new(filename, rock, usage, fmode);
+ frefid_t f = fileref_new(filename, NULL, rock, usage, fmode);
g_free(filename);
gtk_widget_destroy(chooser);
* glkunix_set_base_file(), and otherwise in the current working directory.
* </para></note>
*
- * Since filenames are highly platform-specific, you should use
- * glk_fileref_create_by_name() with care. It is legal to pass any string in the
- * name argument. However, the library may have to mangle, transform, or
- * truncate the string to make it a legal native filename.
+ * Earlier versions of the Glk spec specified that the library may have to
+ * extend, truncate, or change your name argument in order to produce a legal
+ * native filename. This remains true. However, since Glk was originally
+ * proposed, the world has largely reached consensus about what a filename looks
+ * like. Therefore, it is worth including some recommended library behavior
+ * here. Libraries that share this behavior will more easily be able to exchange
+ * files, which may be valuable both to authors (distributing data files for
+ * games) and for players (moving data between different computers or different
+ * applications).
+ *
+ * The library should take the given filename argument, and delete any
+ * characters illegal for a filename. This will include all of the following
+ * characters (and more, if the OS requires it): slash, backslash, angle
+ * brackets (less-than and greater-than), colon, double-quote, pipe (vertical
+ * bar), question-mark, asterisk. The library should also truncate the argument
+ * at the first period (delete the first period and any following characters).
+ * If the result is the empty string, change it to the string
+ * <code>"null"</code>.
+ *
+ * It should then append an appropriate suffix, depending on the usage:
+ * <filename>.glkdata</filename> for %fileusage_Data,
+ * <filename>.glksave</filename> for %fileusage_SavedGame,
+ * <filename>.txt</filename> for %fileusage_Transcript and
+ * %fileusage_InputRecord.
+ *
+ * The above behavior is not a requirement of the Glk spec. Older
+ * implementations can continue doing what they do. Some programs (e.g.
+ * web-based interpreters) may not have access to a traditional filesystem at
+ * all, and to them these recommendations will be meaningless.
+ *
+ * On the other side of the coin, the game file should not press these
+ * limitations. Best practice is for the game to pass a filename containing only
+ * letters and digits, beginning with a letter, and not mixing upper and lower
+ * case. Avoid overly-long filenames.
*
* <note><para>
- * For example, if you create two filerefs with the names <quote>File</quote>
- * and <quote>FILE</quote>, they may wind up pointing to the same file; the
- * platform may not support case distinctions in file names. Another example:
- * on a platform where file type is specified by filename suffix, the library
- * will add an appropriate suffix based on the usage; any suffix in the string
- * will be overwritten or added to. For that matter, remember that the period
- * is not a legal character in Acorn filenames...
+ * The earlier Glk spec gave more stringent recommendations: <quote>No more
+ * than 8 characters, consisting entirely of upper-case letters and numbers,
+ * starting with a letter</quote>. The DOS era is safely contained, if not
+ * over, so this has been relaxed. The I7 manual recommends <quote>23
+ * characters or fewer</quote>.
* </para></note>
*
- * The most conservative approach is to pass a string of no more than 8
- * characters, consisting entirely of upper-case letters and numbers, starting
- * with a letter. You can then be reasonably sure that the resulting filename
- * will display all the characters you specify — in some form.
+ * <note><para>
+ * To address other complications:</para>
+ * <itemizedlist>
+ * <listitem><para>
+ * Some filesystems are case-insensitive. If you create two filerefs with
+ * the names <filename>File</filename> and <filename>FILE</filename>, they
+ * may wind up pointing to the same file, or they may not. Avoid doing
+ * this.
+ * </para></listitem>
+ * <listitem><para>
+ * Some programs will look for all files in the same directory as the
+ * program itself (or, for interpreted games, in the same directory as the
+ * game file). Others may keep files in a data-specific directory
+ * appropriate for the user (e.g., <filename
+ * class="directory">~/Library</filename> on MacOS).
+ * </para></listitem>
+ * <listitem><para>
+ * If a game interpreter uses a data-specific directory, there is a
+ * question of whether to use a common location, or divide it into
+ * game-specific subdirectories. (Or to put it another way: should the
+ * namespace of named files be per-game or app-wide?) Since data files may
+ * be exchanged between games, they should be given an app-wide namespace.
+ * In contrast, saved games should be per-game, as they can never be
+ * exchanged. Transcripts and input records can go either way.
+ * </para></listitem>
+ * <listitem><para>
+ * When updating an older library to follow these recommendations,
+ * consider backwards compatibility for games already installed. When
+ * opening an existing file (that is, not in a write-only mode) it may be
+ * worth looking under the older name (suffix) if the newer one does not
+ * already exist.
+ * </para></listitem>
+ * <listitem><para>
+ * Game-save files are already stored with a variety of file suffixes,
+ * since that usage goes back to the oldest IF interpreters, long
+ * predating Glk. It is reasonable to treat them in some special way,
+ * while hewing closer to these recommendations for data files.
+ * </para></listitem>
+ * </itemizedlist>
+ * </note>
*
* Returns: A new fileref, or %NULL if the fileref creation failed.
*/
ChimaraGlkPrivate *glk_data = g_private_get(glk_data_key);
/* Do any string-munging here to remove illegal Latin-1 characters from
- filename. On ext3, the only illegal characters are '/' and '\0'. */
- g_strdelimit(name, "/", '_');
+ filename. On ext3, the only illegal characters are '/' and '\0', but the Glk
+ spec calls for removing any other tricky characters. */
+ char *buf = g_malloc(strlen(name));
+ char *ptr, *filename, *extension;
+ int len;
+ for(ptr = name, len = 0; *ptr && *ptr != '.'; ptr++)
+ {
+ switch(*ptr)
+ {
+ case '"': case '\\': case '/': case '>': case '<':
+ case ':': case '|': case '?': case '*':
+ break;
+ default:
+ buf[len++] = *ptr;
+ }
+ }
+ buf[len] = '\0';
+
+ /* If there is nothing left, make the name "null" */
+ if(len == 0) {
+ strcpy(buf, "null");
+ len = strlen(buf);
+ }
+
+ switch(usage & fileusage_TypeMask)
+ {
+ case fileusage_Data:
+ extension = ".glkdata";
+ break;
+ case fileusage_SavedGame:
+ extension = ".glksave";
+ break;
+ case fileusage_InputRecord:
+ case fileusage_Transcript:
+ extension = ".txt";
+ break;
+ default:
+ ILLEGAL_PARAM("Unknown file usage: %u", usage);
+ return NULL;
+ }
+ filename = g_strconcat(buf, extension, NULL);
/* Find out what encoding filenames are in */
const gchar **charsets; /* Do not free */
/* Convert name to that encoding */
GError *error = NULL;
- gchar *osname = g_convert(name, -1, charsets[0], "ISO-8859-1", NULL, NULL,
- &error);
+ char *osname = g_convert(filename, -1, charsets[0], "ISO-8859-1", NULL, NULL, &error);
if(osname == NULL)
{
WARNING_S("Error during latin1->filename conversion", error->message);
path = g_strdup(osname);
g_free(osname);
- frefid_t f = fileref_new(path, rock, usage, filemode_ReadWrite);
+ frefid_t f = fileref_new(path, buf, rock, usage, filemode_ReadWrite);
g_free(path);
+ g_free(buf);
return f;
}
* not point to the same actual disk file.
*
* <note><para>
- * This generally depends on whether the platform uses suffixes to indicate
- * file type.
+ * Most platforms use suffixes to indicate file type, so it typically will
+ * not. See the earlier comments about recommended file suffixes.
* </para></note>
*
* If you do this, and open both file references for writing, the results are
glk_fileref_create_from_fileref(glui32 usage, frefid_t fref, glui32 rock)
{
VALID_FILEREF(fref, return NULL);
- return fileref_new(fref->filename, rock, usage, fref->orig_filemode);
+ return fileref_new(fref->filename, fref->basename, rock, usage, fref->orig_filemode);
}
/**
/* Fileref parameters */
gchar *filename; /* Always stored in the default filename encoding, not
UTF8 or Latin-1 */
+ char *basename; /* Name from which real filename was derived */
glui32 orig_filemode; /* Used to check if the user gets a fileref in read
mode and then tries to open it in write mode */
glui32 usage;
};
-G_GNUC_INTERNAL frefid_t fileref_new(gchar *filename, glui32 rock, glui32 usage, glui32 orig_filemode);
+G_GNUC_INTERNAL frefid_t fileref_new(char *filename, char *basename, glui32 rock, glui32 usage, glui32 orig_filemode);
#endif
/* Version of the Glk specification implemented by this library */
#define MAJOR_VERSION 0
#define MINOR_VERSION 7
-#define SUB_VERSION 2
+#define SUB_VERSION 4
/**
* glk_gestalt:
case gestalt_UnicodeNorm:
case gestalt_LineInputEcho:
case gestalt_LineTerminators:
+ case gestalt_ResourceStream:
return 1;
/* Capabilities supported if compiled with GStreamer */
case gestalt_SoundVolume:
case gestalt_SoundNotify:
case gestalt_SoundMusic:
+ case gestalt_Sound2:
#ifdef GSTREAMER_SOUND
return 1;
#else
#endif
/* Unsupported capabilities */
- /* none! */
+ /* None! */
/* Selector not supported */
default:
/* gi_blorb.c: Blorb library layer for Glk API.
- gi_blorb version 1.4.
+ gi_blorb version 1.5.
Designed by Andrew Plotkin <erkyrath@eblong.com>
- http://www.eblong.com/zarf/glk/index.html
+ http://eblong.com/zarf/glk/
- This file is copyright 1998-2000 by Andrew Plotkin. You may copy,
+ This file is copyright 1998-2010 by Andrew Plotkin. You may copy,
distribute, and incorporate it into your own programs, by any means
and under any conditions, as long as you do not modify it. You may
also modify this file, incorporate it into your own programs,
#define _GI_BLORB_H
/* gi_blorb.h: Blorb library layer for Glk API.
- gi_blorb version 1.4.
+ gi_blorb version 1.5.
Designed by Andrew Plotkin <erkyrath@eblong.com>
- http://www.eblong.com/zarf/glk/index.html
+ http://eblong.com/zarf/glk/
- This file is copyright 1998-2000 by Andrew Plotkin. You may copy,
+ This file is copyright 1998-2012 by Andrew Plotkin. You may copy,
distribute, and incorporate it into your own programs, by any means
and under any conditions, as long as you do not modify it. You may
also modify this file, incorporate it into your own programs,
#define giblorb_make_id(c1, c2, c3, c4) \
(((c1) << 24) | ((c2) << 16) | ((c3) << 8) | (c4))
-#define giblorb_ID_Snd (giblorb_make_id('S', 'n', 'd', ' '))
#define giblorb_ID_Exec (giblorb_make_id('E', 'x', 'e', 'c'))
+#define giblorb_ID_Snd (giblorb_make_id('S', 'n', 'd', ' '))
#define giblorb_ID_Pict (giblorb_make_id('P', 'i', 'c', 't'))
+#define giblorb_ID_Data (giblorb_make_id('D', 'a', 't', 'a'))
#define giblorb_ID_Copyright (giblorb_make_id('(', 'c', ')', ' '))
#define giblorb_ID_AUTH (giblorb_make_id('A', 'U', 'T', 'H'))
#define giblorb_ID_ANNO (giblorb_make_id('A', 'N', 'N', 'O'))
+#define giblorb_ID_TEXT (giblorb_make_id('T', 'E', 'X', 'T'))
+#define giblorb_ID_BINA (giblorb_make_id('B', 'I', 'N', 'A'))
/* giblorb_map_t: Holds the complete description of an open Blorb
file. This type is opaque for normal interpreter use. */
-/* gi_dispa.c: Dispatch layer for Glk API, version 0.7.2.
+/* gi_dispa.c: Dispatch layer for Glk API, version 0.7.4.
Designed by Andrew Plotkin <erkyrath@eblong.com>
http://eblong.com/zarf/glk/
- This file is copyright 1998-2011 by Andrew Plotkin. You may copy,
+ This file is copyright 1998-2012 by Andrew Plotkin. You may copy,
distribute, and incorporate it into your own programs, by any means
and under any conditions, as long as you do not modify it. You may
also modify this file, incorporate it into your own programs,
{ "evtype_Redraw", (6) },
{ "evtype_SoundNotify", (7) },
{ "evtype_Timer", (1) },
+ { "evtype_VolumeNotify", (9) },
{ "filemode_Read", (0x02) },
{ "filemode_ReadWrite", (0x03) },
{ "gestalt_LineTerminatorKey", (19) },
{ "gestalt_LineTerminators", (18) },
{ "gestalt_MouseInput", (4) },
+ { "gestalt_ResourceStream", (22) },
{ "gestalt_Sound", (8) },
+ { "gestalt_Sound2", (21) },
{ "gestalt_SoundMusic", (13) },
{ "gestalt_SoundNotify", (10) },
{ "gestalt_SoundVolume", (9) },
{ 0x00FA, glk_schannel_stop, "schannel_stop" },
{ 0x00FB, glk_schannel_set_volume, "schannel_set_volume" },
{ 0x00FC, glk_sound_load_hint, "sound_load_hint" },
+#ifdef GLK_MODULE_SOUND2
+ { 0x00F4, glk_schannel_create_ext, "schannel_create_ext" },
+ { 0x00F7, glk_schannel_play_multi, "schannel_play_multi" },
+ { 0x00FD, glk_schannel_set_volume_ext, "schannel_set_volume_ext" },
+ { 0x00FE, glk_schannel_pause, "schannel_pause" },
+ { 0x00FF, glk_schannel_unpause, "schannel_unpause" },
+#endif /* GLK_MODULE_SOUND2 */
#endif /* GLK_MODULE_SOUND */
#ifdef GLK_MODULE_HYPERLINKS
{ 0x0100, glk_set_hyperlink, "set_hyperlink" },
{ 0x016E, glk_date_to_simple_time_utc, "date_to_simple_time_utc" },
{ 0x016F, glk_date_to_simple_time_local, "date_to_simple_time_local" },
#endif /* GLK_MODULE_DATETIME */
+#ifdef GLK_MODULE_RESOURCE_STREAM
+ { 0x0049, glk_stream_open_resource, "stream_open_resource" },
+ { 0x013A, glk_stream_open_resource_uni, "stream_open_resource_uni" },
+#endif /* GLK_MODULE_RESOURCE_STREAM */
};
glui32 gidispatch_count_classes()
return "2QdIu:";
case 0x00FC: /* sound_load_hint */
return "2IuIu:";
+
+#ifdef GLK_MODULE_SOUND2
+ case 0x00F4: /* schannel_create_ext */
+ return "3IuIu:Qd";
+ case 0x00F7: /* schannel_play_multi */
+ return "4>+#Qd>+#IuIu:Iu";
+ case 0x00FD: /* schannel_set_volume_ext */
+ return "4QdIuIuIu:";
+ case 0x00FE: /* schannel_pause */
+ return "1Qd:";
+ case 0x00FF: /* schannel_unpause */
+ return "1Qd:";
+#endif /* GLK_MODULE_SOUND2 */
#endif /* GLK_MODULE_SOUND */
#ifdef GLK_MODULE_HYPERLINKS
return "3>+[8IsIsIsIsIsIsIsIs]Iu:Is";
#endif /* GLK_MODULE_DATETIME */
+#ifdef GLK_MODULE_RESOURCE_STREAM
+ case 0x0049: /* stream_open_resource */
+ return "3IuIu:Qb";
+ case 0x013A: /* stream_open_resource_uni */
+ return "3IuIu:Qb";
+#endif /* GLK_MODULE_RESOURCE_STREAM */
+
default:
return NULL;
}
case 0x00FC: /* sound_load_hint */
glk_sound_load_hint(arglist[0].uint, arglist[1].uint);
break;
+
+#ifdef GLK_MODULE_SOUND2
+ case 0x00F4: /* schannel_create_ext */
+ arglist[3].opaqueref = glk_schannel_create_ext(arglist[0].uint, arglist[1].uint);
+ break;
+ case 0x00F7: /* schannel_play_multi */
+ if (arglist[0].ptrflag && arglist[3].ptrflag)
+ arglist[8].uint = glk_schannel_play_multi(arglist[1].array, arglist[2].uint, arglist[4].array, arglist[5].uint, arglist[6].uint);
+ else if (arglist[0].ptrflag)
+ arglist[6].uint = glk_schannel_play_multi(arglist[1].array, arglist[2].uint, NULL, 0, arglist[4].uint);
+ else if (arglist[1].ptrflag)
+ arglist[6].uint = glk_schannel_play_multi(NULL, 0, arglist[2].array, arglist[3].uint, arglist[4].uint);
+ else
+ arglist[4].uint = glk_schannel_play_multi(NULL, 0, NULL, 0, arglist[2].uint);
+ break;
+ case 0x00FD: /* schannel_set_volume_ext */
+ glk_schannel_set_volume_ext(arglist[0].opaqueref, arglist[1].uint, arglist[2].uint, arglist[3].uint);
+ break;
+ case 0x00FE: /* schannel_pause */
+ glk_schannel_pause(arglist[0].opaqueref);
+ break;
+ case 0x00FF: /* schannel_unpause */
+ glk_schannel_unpause(arglist[0].opaqueref);
+ break;
+#endif /* GLK_MODULE_SOUND2 */
#endif /* GLK_MODULE_SOUND */
#ifdef GLK_MODULE_HYPERLINKS
break;
#endif /* GLK_MODULE_DATETIME */
+#ifdef GLK_MODULE_RESOURCE_STREAM
+ case 0x0049: /* stream_open_resource */
+ arglist[3].opaqueref = glk_stream_open_resource(arglist[0].uint, arglist[1].uint);
+ break;
+ case 0x013A: /* stream_open_resource_uni */
+ arglist[3].opaqueref = glk_stream_open_resource_uni(arglist[0].uint, arglist[1].uint);
+ break;
+#endif /* GLK_MODULE_RESOURCE_STREAM */
+
default:
/* do nothing */
break;
#ifndef _GI_DISPA_H
#define _GI_DISPA_H
-/* gi_dispa.h: Header file for dispatch layer of Glk API, version 0.7.2.
+/* gi_dispa.h: Header file for dispatch layer of Glk API, version 0.7.4.
Designed by Andrew Plotkin <erkyrath@eblong.com>
- http://www.eblong.com/zarf/glk/index.html
+ http://eblong.com/zarf/glk/index.html
- This file is copyright 1998-2011 by Andrew Plotkin. You may copy,
+ This file is copyright 1998-2012 by Andrew Plotkin. You may copy,
distribute, and incorporate it into your own programs, by any means
and under any conditions, as long as you do not modify it. You may
also modify this file, incorporate it into your own programs,
#ifndef GLK_H
#define GLK_H
-/* glk.h: Header file for Glk API, version 0.7.2.
- Designed by Andrew Plotkin <erkyrath@eblong.com>
- http://eblong.com/zarf/glk/
-
- This file is copyright 1998-2011 by Andrew Plotkin. You may copy,
- distribute, and incorporate it into your own programs, by any means
- and under any conditions, as long as you do not modify it. You may
- also modify this file, incorporate it into your own programs,
- and distribute the modified version, as long as you retain a notice
- in your program or documentation which mentions my name and the URL
- shown above.
- */
+/* glk.h: Header file for Glk API, version 0.7.4.
+ Designed by Andrew Plotkin <erkyrath@eblong.com>
+ http://eblong.com/zarf/glk/
+
+ This file is copyright 1998-2012 by Andrew Plotkin. You may copy,
+ distribute, and incorporate it into your own programs, by any means
+ and under any conditions, as long as you do not modify it. You may
+ also modify this file, incorporate it into your own programs,
+ and distribute the modified version, as long as you retain a notice
+ in your program or documentation which mentions my name and the URL
+ shown above.
+*/
/* If your system does not have <stdint.h>, you'll have to remove this
- include line. Then edit the definition of glui32 to make sure it's
- really a 32-bit unsigned integer type, and glsi32 to make sure
- it's really a 32-bit signed integer type. If they're not, horrible
- things will happen. */
+ include line. Then edit the definition of glui32 to make sure it's
+ really a 32-bit unsigned integer type, and glsi32 to make sure
+ it's really a 32-bit signed integer type. If they're not, horrible
+ things will happen. */
#include <stdint.h>
typedef uint32_t glui32;
typedef int32_t glsi32;
/* These are the compile-time conditionals that reveal various Glk optional
- modules. */
+ modules. Note that if GLK_MODULE_SOUND2 is defined, GLK_MODULE_SOUND
+ must be also. */
#define GLK_MODULE_LINE_ECHO
#define GLK_MODULE_LINE_TERMINATORS
#define GLK_MODULE_UNICODE
#define GLK_MODULE_UNICODE_NORM
#define GLK_MODULE_IMAGE
#define GLK_MODULE_SOUND
+#define GLK_MODULE_SOUND2
#define GLK_MODULE_HYPERLINKS
#define GLK_MODULE_DATETIME
+#define GLK_MODULE_RESOURCE_STREAM
/* These types are opaque object identifiers. They're pointers to opaque
- C structures, which are defined differently by each library. */
+ C structures, which are defined differently by each library. */
typedef struct glk_window_struct *winid_t;
typedef struct glk_stream_struct *strid_t;
typedef struct glk_fileref_struct *frefid_t;
#define gestalt_LineTerminators (18)
#define gestalt_LineTerminatorKey (19)
#define gestalt_DateTime (20)
+#define gestalt_Sound2 (21)
+#define gestalt_ResourceStream (22)
#define evtype_None (0)
#define evtype_Timer (1)
#define evtype_Redraw (6)
#define evtype_SoundNotify (7)
#define evtype_Hyperlink (8)
+#define evtype_VolumeNotify (9)
typedef struct event_struct {
glui32 type;
#define stylehint_just_RightFlush (3)
/* glk_main() is the top-level function which you define. The Glk library
- calls it. */
+ calls it. */
extern void glk_main(void);
extern void glk_exit(void);
extern glui32 glk_gestalt(glui32 sel, glui32 val);
extern glui32 glk_gestalt_ext(glui32 sel, glui32 val, glui32 *arr,
- glui32 arrlen);
+ glui32 arrlen);
extern unsigned char glk_char_to_lower(unsigned char ch);
extern unsigned char glk_char_to_upper(unsigned char ch);
extern winid_t glk_window_get_root(void);
extern winid_t glk_window_open(winid_t split, glui32 method, glui32 size,
- glui32 wintype, glui32 rock);
+ glui32 wintype, glui32 rock);
extern void glk_window_close(winid_t win, stream_result_t *result);
extern void glk_window_get_size(winid_t win, glui32 *widthptr,
- glui32 *heightptr);
+ glui32 *heightptr);
extern void glk_window_set_arrangement(winid_t win, glui32 method,
- glui32 size, winid_t keywin);
+ glui32 size, winid_t keywin);
extern void glk_window_get_arrangement(winid_t win, glui32 *methodptr,
- glui32 *sizeptr, winid_t *keywinptr);
+ glui32 *sizeptr, winid_t *keywinptr);
extern winid_t glk_window_iterate(winid_t win, glui32 *rockptr);
extern glui32 glk_window_get_rock(winid_t win);
extern glui32 glk_window_get_type(winid_t win);
extern void glk_set_window(winid_t win);
extern strid_t glk_stream_open_file(frefid_t fileref, glui32 fmode,
- glui32 rock);
+ glui32 rock);
extern strid_t glk_stream_open_memory(char *buf, glui32 buflen, glui32 fmode,
- glui32 rock);
+ glui32 rock);
extern void glk_stream_close(strid_t str, stream_result_t *result);
extern strid_t glk_stream_iterate(strid_t str, glui32 *rockptr);
extern glui32 glk_stream_get_rock(strid_t str);
extern glui32 glk_get_buffer_stream(strid_t str, char *buf, glui32 len);
extern void glk_stylehint_set(glui32 wintype, glui32 styl, glui32 hint,
- glsi32 val);
+ glsi32 val);
extern void glk_stylehint_clear(glui32 wintype, glui32 styl, glui32 hint);
extern glui32 glk_style_distinguish(winid_t win, glui32 styl1, glui32 styl2);
extern glui32 glk_style_measure(winid_t win, glui32 styl, glui32 hint,
- glui32 *result);
+ glui32 *result);
extern frefid_t glk_fileref_create_temp(glui32 usage, glui32 rock);
extern frefid_t glk_fileref_create_by_name(glui32 usage, char *name,
- glui32 rock);
+ glui32 rock);
extern frefid_t glk_fileref_create_by_prompt(glui32 usage, glui32 fmode,
- glui32 rock);
+ glui32 rock);
extern frefid_t glk_fileref_create_from_fileref(glui32 usage, frefid_t fref,
- glui32 rock);
+ glui32 rock);
extern void glk_fileref_destroy(frefid_t fref);
extern frefid_t glk_fileref_iterate(frefid_t fref, glui32 *rockptr);
extern glui32 glk_fileref_get_rock(frefid_t fref);
extern void glk_request_timer_events(glui32 millisecs);
extern void glk_request_line_event(winid_t win, char *buf, glui32 maxlen,
- glui32 initlen);
+ glui32 initlen);
extern void glk_request_char_event(winid_t win);
extern void glk_request_mouse_event(winid_t win);
#ifdef GLK_MODULE_LINE_TERMINATORS
extern void glk_set_terminators_line_event(winid_t win, glui32 *keycodes,
- glui32 count);
+ glui32 count);
#endif /* GLK_MODULE_LINE_TERMINATORS */
#ifdef GLK_MODULE_UNICODE
extern glui32 glk_buffer_to_lower_case_uni(glui32 *buf, glui32 len,
- glui32 numchars);
+ glui32 numchars);
extern glui32 glk_buffer_to_upper_case_uni(glui32 *buf, glui32 len,
- glui32 numchars);
+ glui32 numchars);
extern glui32 glk_buffer_to_title_case_uni(glui32 *buf, glui32 len,
- glui32 numchars, glui32 lowerrest);
+ glui32 numchars, glui32 lowerrest);
extern void glk_put_char_uni(glui32 ch);
extern void glk_put_string_uni(glui32 *s);
extern glui32 glk_get_line_stream_uni(strid_t str, glui32 *buf, glui32 len);
extern strid_t glk_stream_open_file_uni(frefid_t fileref, glui32 fmode,
- glui32 rock);
+ glui32 rock);
extern strid_t glk_stream_open_memory_uni(glui32 *buf, glui32 buflen,
- glui32 fmode, glui32 rock);
+ glui32 fmode, glui32 rock);
extern void glk_request_char_event_uni(winid_t win);
extern void glk_request_line_event_uni(winid_t win, glui32 *buf,
- glui32 maxlen, glui32 initlen);
+ glui32 maxlen, glui32 initlen);
#endif /* GLK_MODULE_UNICODE */
#ifdef GLK_MODULE_UNICODE_NORM
extern glui32 glk_buffer_canon_decompose_uni(glui32 *buf, glui32 len,
- glui32 numchars);
+ glui32 numchars);
extern glui32 glk_buffer_canon_normalize_uni(glui32 *buf, glui32 len,
- glui32 numchars);
+ glui32 numchars);
#endif /* GLK_MODULE_UNICODE_NORM */
extern glui32 glk_image_draw(winid_t win, glui32 image, glsi32 val1, glsi32 val2);
extern glui32 glk_image_draw_scaled(winid_t win, glui32 image,
- glsi32 val1, glsi32 val2, glui32 width, glui32 height);
+ glsi32 val1, glsi32 val2, glui32 width, glui32 height);
extern glui32 glk_image_get_info(glui32 image, glui32 *width, glui32 *height);
extern void glk_window_flow_break(winid_t win);
extern void glk_window_erase_rect(winid_t win,
- glsi32 left, glsi32 top, glui32 width, glui32 height);
+ glsi32 left, glsi32 top, glui32 width, glui32 height);
extern void glk_window_fill_rect(winid_t win, glui32 color,
- glsi32 left, glsi32 top, glui32 width, glui32 height);
+ glsi32 left, glsi32 top, glui32 width, glui32 height);
extern void glk_window_set_background_color(winid_t win, glui32 color);
#endif /* GLK_MODULE_IMAGE */
extern glui32 glk_schannel_play(schanid_t chan, glui32 snd);
extern glui32 glk_schannel_play_ext(schanid_t chan, glui32 snd, glui32 repeats,
- glui32 notify);
+ glui32 notify);
extern void glk_schannel_stop(schanid_t chan);
extern void glk_schannel_set_volume(schanid_t chan, glui32 vol);
extern void glk_sound_load_hint(glui32 snd, glui32 flag);
+#ifdef GLK_MODULE_SOUND2
+/* Note that this section is nested inside the #ifdef GLK_MODULE_SOUND.
+ GLK_MODULE_SOUND must be defined if GLK_MODULE_SOUND2 is. */
+
+extern schanid_t glk_schannel_create_ext(glui32 rock, glui32 volume);
+extern glui32 glk_schannel_play_multi(schanid_t *chanarray, glui32 chancount,
+ glui32 *sndarray, glui32 soundcount, glui32 notify);
+extern void glk_schannel_pause(schanid_t chan);
+extern void glk_schannel_unpause(schanid_t chan);
+extern void glk_schannel_set_volume_ext(schanid_t chan, glui32 vol,
+ glui32 duration, glui32 notify);
+
+#endif /* GLK_MODULE_SOUND2 */
#endif /* GLK_MODULE_SOUND */
#ifdef GLK_MODULE_HYPERLINKS
extern void glk_time_to_date_utc(glktimeval_t *time, glkdate_t *date);
extern void glk_time_to_date_local(glktimeval_t *time, glkdate_t *date);
extern void glk_simple_time_to_date_utc(glsi32 time, glui32 factor,
- glkdate_t *date);
+ glkdate_t *date);
extern void glk_simple_time_to_date_local(glsi32 time, glui32 factor,
- glkdate_t *date);
+ glkdate_t *date);
extern void glk_date_to_time_utc(glkdate_t *date, glktimeval_t *time);
extern void glk_date_to_time_local(glkdate_t *date, glktimeval_t *time);
extern glsi32 glk_date_to_simple_time_utc(glkdate_t *date, glui32 factor);
#endif /* GLK_MODULE_DATETIME */
+#ifdef GLK_MODULE_RESOURCE_STREAM
+
+extern strid_t glk_stream_open_resource(glui32 filenum, glui32 rock);
+extern strid_t glk_stream_open_resource_uni(glui32 filenum, glui32 rock);
+
+#endif /* GLK_MODULE_RESOURCE_STREAM */
+
#endif /* GLK_H */
g_return_val_if_fail(pathname, NULL);
g_return_val_if_fail(strlen(pathname) > 0, NULL);
- frefid_t fileref = fileref_new(pathname, rock,
+ frefid_t fileref = fileref_new(pathname, NULL, rock,
textmode? fileusage_TextMode : fileusage_BinaryMode,
writemode? filemode_Write : filemode_Read);
return file_stream_new(fileref, writemode? filemode_Write : filemode_Read, rock, FALSE);
g_return_val_if_fail(pathname, NULL);
g_return_val_if_fail(strlen(pathname) > 0, NULL);
- frefid_t fileref = fileref_new(pathname, rock, textmode? fileusage_TextMode : fileusage_BinaryMode, filemode_Read);
+ frefid_t fileref = fileref_new(pathname, NULL, rock, textmode? fileusage_TextMode : fileusage_BinaryMode, filemode_Read);
return file_stream_new(fileref, filemode_Read, rock, FALSE);
}
g_slist_free(arglist);
return TRUE;
-}
\ No newline at end of file
+}
g_mutex_unlock(glk_data->resource_lock);
info->pixbuf = gdk_pixbuf_loader_get_pixbuf(loader);
- gdk_pixbuf_ref(info->pixbuf);
+ g_object_ref(info->pixbuf);
g_object_unref(loader);
return info;
g_free(info);
return NULL;
}
- gdk_pixbuf_ref(info->pixbuf);
+ g_object_ref(info->pixbuf);
return info;
}
if( g_slist_length(glk_data->image_cache) >= IMAGE_CACHE_MAX_NUM ) {
struct image_info *head = (struct image_info*) glk_data->image_cache->data;
- gdk_pixbuf_unref(head->pixbuf);
+ g_object_unref(head->pixbuf);
g_free(head);
glk_data->image_cache = g_slist_remove_link(glk_data->image_cache, glk_data->image_cache);
}
void
clear_image_cache(struct image_info *data, gpointer user_data)
{
- gdk_pixbuf_unref(data->pixbuf);
+ g_object_unref(data->pixbuf);
g_free(data);
}
#include "resource.h"
#include "event.h"
+#define VOLUME_TIMER_RESOLUTION 1.0 /* In milliseconds */
+
extern GPrivate *glk_data_key;
#ifdef GSTREAMER_SOUND
* Remember that it is possible that the library will be unable to create a new
* channel, in which case glk_schannel_create() will return %NULL.
*
+ * When you create a channel using glk_schannel_create(), it has full volume,
+ * represented by the value 0x10000. Half volume would be 0x8000, three-quarters
+ * volume would be 0xC000, and so on. A volume of zero represents silence.
+ *
+ * You can overdrive the volume of a channel by setting a volume greater than
+ * 0x10000. However, this is not recommended; the library may be unable to
+ * increase the volume past full, or the sound may become distorted. You should
+ * always create sound resources with the maximum volume you will need, and then
+ * reduce the volume when appropriate using the channel-volume calls.
+ *
+ * <note><para>
+ * Mathematically, these volume changes should be taken as linear
+ * multiplication of a waveform represented as linear samples. As I
+ * understand it, linear PCM encodes the sound pressure, and therefore a
+ * volume of 0x8000 should represent a 6 dB drop.
+ * </para></note>
+ *
* Returns: A new sound channel, or %NULL.
*/
schanid_t
glk_schannel_create(glui32 rock)
+{
+ return glk_schannel_create_ext(rock, 0x10000);
+}
+
+/**
+ * glk_schannel_create_ext:
+ * @rock: The rock value to give the new sound channel.
+ * @volume: Integer representing the volume; 0x10000 is 100%.
+ *
+ * The glk_schannel_create_ext() call lets you create a channel with the volume
+ * already set at a given level.
+ *
+ * Not all libraries support glk_schannel_create_ext(). You should test the
+ * %gestalt_Sound2 selector before you rely on it; see <link
+ * linkend="chimara-Testing-for-Sound-Capabilities">Testing for Sound
+ * Capabilities</link>.
+ *
+ * Returns: A new sound channel, or %NULL.
+ */
+schanid_t
+glk_schannel_create_ext(glui32 rock, glui32 volume)
{
#ifdef GSTREAMER_SOUND
ChimaraGlkPrivate *glk_data = g_private_get(glk_data_key);
goto fail;
}
+ /* Set the initial volume */
+ glk_schannel_set_volume(s, volume);
+
/* Put the elements in the pipeline and link as many together as we can
without knowing the type of the audio stream */
gst_bin_add_many(GST_BIN(s->pipeline), s->source, s->typefind, s->convert, s->filter, s->sink, NULL);
* sound is playing, there will be no notification event.
*
* Not all libraries support sound notification. You should test the
- * %gestalt_SoundNotify selector before you rely on it; see <link
+ * %gestalt_Sound2 selector before you rely on it; see <link
* linkend="chimara-Testing-for-Sound-Capabilities">Testing for Sound
* Capabilities</link>.
+ *
+ * Note that you can play a sound on a channel whose volume is zero. This has
+ * no audible result, unless you later change the volume; but it produces
+ * notifications as usual. You can also play a sound on a paused channel; the
+ * sound is paused immediately, and does not progress.
*
* Returns: 1 on success, 0 on failure.
*/
glk_schannel_play_ext(schanid_t chan, glui32 snd, glui32 repeats, glui32 notify)
{
VALID_SCHANNEL(chan, return 0);
- g_printerr("Play sound %d with repeats %d and notify %d\n", snd, repeats, notify);
#ifdef GSTREAMER_SOUND
ChimaraGlkPrivate *glk_data = g_private_get(glk_data_key);
GInputStream *stream;
g_object_set(chan->source, "stream", stream, NULL);
g_object_unref(stream); /* Now owned by GStreamer element */
- if(!gst_element_set_state(chan->pipeline, GST_STATE_PLAYING)) {
- WARNING_S(_("Could not set GstElement state to"), "PLAYING");
+ /* Play the sound; unless the channel is paused, then pause it instead */
+ if(!gst_element_set_state(chan->pipeline, chan->paused? GST_STATE_PAUSED : GST_STATE_PLAYING)) {
+ WARNING_S(_("Could not set GstElement state to"), chan->paused? "PAUSED" : "PLAYING");
return 0;
}
return 1;
#endif
}
+/**
+ * glk_schannel_play_multi:
+ * @chanarray: Array of #schanid_t structures.
+ * @chancount: Length of @chanarray.
+ * @sndarray: Array of sound resource numbers.
+ * @soundcount: Length of @sndarray, must be equal to @chanarray.
+ * @notify: If nonzero, request a notification when each sound finishes.
+ *
+ * This works the same as glk_schannel_play_ext(), except that you can specify
+ * more than one sound. The channel references and sound resource numbers are
+ * given as two arrays, which must be the same length. The @notify argument
+ * applies to all the sounds; the repeats value for all the sounds is 1.
+ *
+ * All the sounds will begin at exactly the same time.
+ *
+ * This returns the number of sounds that began playing correctly. (This will be
+ * a number from 0 to @soundcount.)
+ *
+ * <note><para>
+ * If the @notify argument is nonzero, you will get a separate sound
+ * notification event as each sound finishes. They will all have the same
+ * @val2 value.
+ * </para></note>
+ * <note><para>
+ * Note that you have to supply @chancount and @soundcount as separate
+ * arguments, even though they are required to be the same. This is an awkward
+ * consequence of the way array arguments are dispatched in Glulx.
+ * </para></note>
+ *
+ * Returns: The number of sounds that started playing correctly.
+ */
+glui32
+glk_schannel_play_multi(schanid_t *chanarray, glui32 chancount, glui32 *sndarray, glui32 soundcount, glui32 notify)
+{
+ g_return_val_if_fail(chancount == soundcount, 0);
+ g_return_val_if_fail(chanarray != NULL || chancount == 0, 0);
+ g_return_val_if_fail(sndarray != NULL || soundcount == 0, 0);
+
+ int count;
+ for(count = 0; count < chancount; count++)
+ VALID_SCHANNEL(chanarray[count], return 0);
+
+#ifdef GSTREAMER_SOUND
+ ChimaraGlkPrivate *glk_data = g_private_get(glk_data_key);
+ GInputStream *stream;
+
+ if(!glk_data->resource_map && !glk_data->resource_load_callback) {
+ WARNING(_("No resource map has been loaded yet."));
+ return 0;
+ }
+
+ /* We keep an array of sounds to skip if any of them have errors */
+ gboolean *skiparray = g_new0(gboolean, chancount);
+
+ /* Set up all the channels one by one */
+ for(count = 0; count < chancount; count++) {
+ /* Stop the previous sound */
+ clean_up_after_playing_sound(chanarray[count]);
+
+ /* Load the sound into a GInputStream, by whatever method */
+ if(!glk_data->resource_map) {
+ gchar *filename = glk_data->resource_load_callback(CHIMARA_RESOURCE_SOUND, sndarray[count], glk_data->resource_load_callback_data);
+ if(!filename) {
+ WARNING(_("Error loading resource from alternative location."));
+ skiparray[count] = TRUE;
+ continue;
+ }
+
+ GError *err = NULL;
+ GFile *file = g_file_new_for_path(filename);
+ stream = G_INPUT_STREAM(g_file_read(file, NULL, &err));
+ if(!stream) {
+ IO_WARNING(_("Error loading resource from file"), filename, err->message);
+ g_free(filename);
+ g_object_unref(file);
+ skiparray[count] = TRUE;
+ continue;
+ }
+ g_free(filename);
+ g_object_unref(file);
+ } else {
+ giblorb_result_t resource;
+ giblorb_err_t result = giblorb_load_resource(glk_data->resource_map, giblorb_method_Memory, &resource, giblorb_ID_Snd, sndarray[count]);
+ if(result != giblorb_err_None) {
+ WARNING_S( _("Error loading resource"), giblorb_get_error_message(result) );
+ skiparray[count] = TRUE;
+ continue;
+ }
+ stream = g_memory_input_stream_new_from_data(resource.data.ptr, resource.length, NULL);
+ }
+
+ chanarray[count]->repeats = 1;
+ chanarray[count]->resource = sndarray[count];
+ chanarray[count]->notify = notify;
+ g_object_set(chanarray[count]->source, "stream", stream, NULL);
+ g_object_unref(stream); /* Now owned by GStreamer element */
+ }
+
+ /* Start all the sounds as close to each other as possible. */
+ /* FIXME: Is there a way to start them exactly at the same time? */
+ glui32 successes = 0;
+ for(count = 0; count < chancount; count++) {
+ if(skiparray[count])
+ continue;
+ /* Play the sound; unless the channel is paused, then pause it instead */
+ if(!gst_element_set_state(chanarray[count]->pipeline, chanarray[count]->paused? GST_STATE_PAUSED : GST_STATE_PLAYING)) {
+ WARNING_S(_("Could not set GstElement state to"), chanarray[count]->paused? "PAUSED" : "PLAYING");
+ skiparray[count] = TRUE;
+ continue;
+ }
+ successes++;
+ }
+ g_free(skiparray);
+ return successes;
+#else
+ return 0;
+#endif
+}
+
/**
* glk_schannel_stop:
* @chan: Channel to silence.
#endif
}
+/**
+ * glk_schannel_pause:
+ * @chan: Channel to pause.
+ *
+ * Pause any sound playing in the channel. This does not generate any
+ * notification events. If the channel is already paused, this does nothing.
+ *
+ * New sounds started in a paused channel are paused immediately.
+ *
+ * A volume change in progress is <emphasis>not</emphasis> paused, and may
+ * proceed to completion, generating a notification if appropriate.
+ */
+void
+glk_schannel_pause(schanid_t chan)
+{
+ VALID_SCHANNEL(chan, return);
+
+ if(chan->paused)
+ return; /* Silently do nothing */
+
+ /* Mark the channel as paused even if there is no sound playing yet */
+ chan->paused = TRUE;
+
+ GstState state;
+ if(gst_element_get_state(chan->pipeline, &state, NULL, GST_CLOCK_TIME_NONE) != GST_STATE_CHANGE_SUCCESS) {
+ WARNING(_("Could not get GstElement state"));
+ return;
+ }
+ if(state != GST_STATE_PLAYING)
+ return; /* Silently do nothing if no sound is playing */
+
+ if(!gst_element_set_state(chan->pipeline, GST_STATE_PAUSED)) {
+ WARNING_S(_("Could not set GstElement state to"), "PAUSED");
+ return;
+ }
+}
+
+/**
+ * glk_schannel_unpause:
+ * @chan: Channel to unpause.
+ *
+ * Unpause the channel. Any paused sounds begin playing where they left off. If
+ * the channel is not already paused, this does nothing.
+ *
+ * <note><para>
+ * This means, for example, that you can pause a channel that is currently
+ * not playing any sounds. If you then add a sound to the channel, it will
+ * not start playing; it will be paused at its beginning. If you later
+ * unpaise the channel, the sound will commence.
+ * </para></note>
+ */
+void
+glk_schannel_unpause(schanid_t chan)
+{
+ VALID_SCHANNEL(chan, return);
+
+ if(!chan->paused)
+ return; /* Silently do nothing */
+
+ /* Mark the channel as not paused in any case */
+ chan->paused = FALSE;
+
+ GstState state;
+ if(gst_element_get_state(chan->pipeline, &state, NULL, GST_CLOCK_TIME_NONE) != GST_STATE_CHANGE_SUCCESS) {
+ WARNING(_("Could not get GstElement state"));
+ return;
+ }
+ if(state != GST_STATE_PAUSED)
+ return; /* Silently do nothing */
+
+ if(!gst_element_set_state(chan->pipeline, GST_STATE_PLAYING)) {
+ WARNING_S(_("Could not set GstElement state to"), "PLAYING");
+ return;
+ }
+}
+
/**
* glk_schannel_set_volume:
* @chan: Channel to set the volume of.
* @vol: Integer representing the volume; 0x10000 is 100%.
*
- * Sets the volume in the channel. When you create a channel, it has full
- * volume, represented by the value 0x10000. Half volume would be 0x8000,
- * three-quarters volume would be 0xC000, and so on. A volume of zero represents
- * silence, although the sound is still considered to be playing.
+ * Sets the volume in the channel, from 0 (silence) to 0x10000 (full volume).
+ * Again, you can overdrive the volume by setting a value greater than 0x10000,
+ * but this is not recommended.
+ *
+ * The glk_schannel_set_volume() function does not include duration and notify
+ * values. Both are assumed to be zero: immediate change, no notification.
*
- * You can call this function between sounds, or while a sound is playing. The
- * effect is immediate.
+ * You can call this function between sounds, or while a sound is playing.
+ * However, a zero-duration change while a sound is playing may produce
+ * unpleasant clicks.
*
- * You can overdrive the volume of a channel by setting a volume greater than
- * 0x10000. However, this is not recommended; the library may be unable to
- * increase the volume past full, or the sound may become distorted. You should
- * always create sound resources with the maximum volume you will need, and then
- * call glk_schannel_set_volume() to reduce the volume when appropriate.
+ * At most one volume change can be occurring on a sound channel at any time.
+ * If you call this function while a previous volume change is in progress, the
+ * previous change is interrupted.
*
* Not all libraries support this function. You should test the
* %gestalt_SoundVolume selector before you rely on it; see <link
*/
void
glk_schannel_set_volume(schanid_t chan, glui32 vol)
+{
+ glk_schannel_set_volume_ext(chan, vol, 0, 0);
+}
+
+static double
+volume_glk_to_gstreamer(glui32 volume_glk)
+{
+ return CLAMP(((double)volume_glk / 0x10000), 0.0, 10.0);
+}
+
+#ifdef GSTREAMER_SOUND
+static gboolean
+volume_change_timeout(schanid_t chan)
+{
+ GTimeVal now;
+ g_get_current_time(&now);
+
+ if(now.tv_sec >= chan->target_time_sec && now.tv_usec >= chan->target_time_usec) {
+ /* We're done - make sure the volume is at the requested level */
+ g_object_set(chan->filter, "volume", chan->target_volume, NULL);
+
+ if(chan->volume_notify)
+ event_throw(chan->glk, evtype_VolumeNotify, NULL, 0, chan->volume_notify);
+
+ chan->volume_timer_id = 0;
+ return FALSE;
+ }
+
+ /* Calculate the appropriate step every time - a busy system may delay or
+ * drop timer ticks */
+ double time_left_msec = (chan->target_time_sec - now.tv_sec) * 1000.0
+ + (chan->target_time_usec - now.tv_usec) / 1000.0;
+ double steps_left = time_left_msec / VOLUME_TIMER_RESOLUTION;
+ double current_volume;
+ g_object_get(chan->filter, "volume", ¤t_volume, NULL);
+ double volume_step = (chan->target_volume - current_volume) / steps_left;
+
+ g_object_set(chan->filter, "volume", current_volume + volume_step, NULL);
+
+ return TRUE;
+}
+#endif /* GSTREAMER_SOUND */
+
+/**
+ * glk_schannel_set_volume_ext:
+ * @chan: Channel to set the volume of.
+ * @vol: Integer representing the volume; 0x10000 is 100%.
+ * @duration: Length of volume change in milliseconds, or 0 for immediate.
+ * @notify: If nonzero, requests a notification when the volume change finishes.
+ *
+ * Sets the volume in the channel, from 0 (silence) to 0x10000 (full volume).
+ * Again, you can overdrive the volume by setting a value greater than 0x10000,
+ * but this is not recommended.
+ *
+ * If the @duration is zero, the change is immediate. Otherwise, the change
+ * begins immediately, and occurs smoothly over the next @duration milliseconds.
+ *
+ * The @notify value should be nonzero in order to request a volume notification
+ * event. If you do this, when the volume change is completed, you will get an
+ * event with type #evtype_VolumeNotify. The window will be %NULL, @val1 will be
+ * zero, and @val2 will be the nonzero value you passed as @notify.
+ *
+ * You can call this function between sounds, or while a sound is playing.
+ * However, a zero-duration change while a sound is playing may produce
+ * unpleasant clicks.
+ *
+ * At most one volume change can be occurring on a sound channel at any time. If
+ * you call this function while a previous volume change is in progress, the
+ * previous change is interrupted. The beginning point of the new volume change
+ * should be wherever the previous volume change was interrupted (rather than
+ * the previous change's beginning or ending point).
+ *
+ * Not all libraries support these functions. You should test the appropriate
+ * gestalt selectors before you rely on them; see "Testing for Sound
+ * Capabilities".
+ */
+void
+glk_schannel_set_volume_ext(schanid_t chan, glui32 vol, glui32 duration, glui32 notify)
{
VALID_SCHANNEL(chan, return);
+ /* Silently ignore out-of-range volume values */
+
#ifdef GSTREAMER_SOUND
- gdouble volume_gst = (gdouble)vol / 0x10000;
- g_printerr("Volume set to: %f\n", volume_gst);
- g_object_set(chan->filter, "volume", CLAMP(volume_gst, 0.0, 10.0), NULL);
+ /* Interrupt a previous volume change */
+ if(chan->volume_timer_id > 0)
+ g_source_remove(chan->volume_timer_id);
+
+ double target_volume = volume_glk_to_gstreamer(vol);
+
+ if(duration == 0) {
+ g_object_set(chan->filter, "volume", target_volume, NULL);
+
+ if(notify != 0)
+ event_throw(chan->glk, evtype_VolumeNotify, NULL, 0, notify);
+
+ return;
+ }
+
+ GTimeVal target_time;
+ g_get_current_time(&target_time);
+ g_time_val_add(&target_time, (long)duration * 1000);
+
+ chan->target_volume = target_volume;
+ chan->target_time_sec = target_time.tv_sec;
+ chan->target_time_usec = target_time.tv_usec;
+ chan->volume_notify = notify;
+
+ /* Set up a timer for the volume */
+ chan->volume_timer_id = g_timeout_add(VOLUME_TIMER_RESOLUTION, (GSourceFunc)volume_change_timeout, chan);
#endif
}
glui32 resource, notify;
/* How many times to repeat the last sound played (-1 = forever) */
glui32 repeats;
+ /* Whether channel is paused */
+ gboolean paused;
+ /* Volume change information */
+ double target_volume;
+ long target_time_sec, target_time_usec;
+ guint volume_timer_id;
+ glui32 volume_notify;
+
#ifdef GSTREAMER_SOUND
/* Each sound channel is represented as a GStreamer pipeline. */
GstElement *pipeline, *source, *typefind, *demux, *decode, *convert, *filter, *sink;
#endif
};
-#endif
\ No newline at end of file
+#endif
+#include <config.h>
#include "stream.h"
#include "fileref.h"
#include "magic.h"
+#include "gi_blorb.h"
#include <errno.h>
#include <stdio.h>
#include <glib.h>
#include <glib/gstdio.h>
+#include <glib/gi18n-lib.h>
#include "chimara-glk-private.h"
extern GPrivate *glk_data_key;
return file_stream_new(fileref, fmode, rock, TRUE);
}
+/**
+ * glk_stream_open_resource:
+ * @filenum: Resource chunk number to open.
+ * @rock: The new stream's rock value.
+ *
+ * Open the given data resource for reading (only), as a normal stream.
+ *
+ * <note><para>
+ * Note that there is no notion of file usage — the resource does not
+ * have to be specified as <quote>saved game</quote> or whatever.
+ * </para></note>
+ *
+ * If no resource chunk of the given number exists, the open function returns
+ * %NULL.
+ *
+ * As with file streams, a binary resource stream reads the resource as bytes. A
+ * text resource stream reads characters encoded as Latin-1.
+ *
+ * When reading from a resource stream, newlines are not remapped, even if they
+ * normally would be when reading from a text file on the host OS. If you read a
+ * line (glk_get_line_stream() or glk_get_line_stream_uni()), a Unix newline
+ * (0x0A) terminates the line.
+ *
+ * Returns: the new stream, or %NULL.
+ */
+strid_t
+glk_stream_open_resource(glui32 filenum, glui32 rock)
+{
+ /* Adapted from CheapGlk */
+ strid_t str;
+ gboolean isbinary;
+ giblorb_err_t err;
+ giblorb_result_t res;
+ giblorb_map_t *map = giblorb_get_resource_map();
+ if(map == NULL) {
+ WARNING(_("Could not create resource stream, because there was no "
+ "resource map."));
+ return NULL; /* Not running from a blorb file */
+ }
+
+ err = giblorb_load_resource(map, giblorb_method_Memory, &res, giblorb_ID_Data, filenum);
+ if(err) {
+ WARNING_S(_("Could not create resource stream, because the resource "
+ "could not be loaded"), giblorb_get_error_message(err));
+ return 0; /* Not found, or some other error */
+ }
+
+ /* We'll use the in-memory copy of the chunk data as the basis for
+ our new stream. It's important to not call chunk_unload() until
+ the stream is closed (and we won't).
+
+ This will be memory-hoggish for giant data chunks, but I don't
+ expect giant data chunks at this point. A more efficient model
+ would be to use the file on disk, but this requires some hacking
+ into the file stream code (we'd need to open a new FILE*) and
+ I don't feel like doing that. */
+
+ if(res.chunktype == giblorb_ID_TEXT)
+ isbinary = FALSE;
+ else if(res.chunktype == giblorb_ID_BINA)
+ isbinary = TRUE;
+ else {
+ WARNING(_("Could not create resource stream, because chunk was of "
+ "unknown type."));
+ return NULL; /* Unknown chunk type */
+ }
+
+ str = stream_new_common(rock);
+ str->type = STREAM_TYPE_RESOURCE;
+ str->file_mode = filemode_Read;
+ str->binary = isbinary;
+
+ if (res.data.ptr && res.length) {
+ str->buffer = res.data.ptr;
+ str->mark = 0;
+ str->buflen = res.length;
+ str->endmark = str->buflen;
+ }
+
+ return str;
+}
+
+/**
+ * glk_stream_open_resource_uni:
+ * @filenum: Resource chunk number to open.
+ * @rock: The new stream's rock value.
+ *
+ * Open the given data resource for reading (only), as a Unicode stream. See
+ * glk_stream_open_resource() for more information.
+ *
+ * As with file streams, a binary resource stream reads the resource as
+ * four-byte (big-endian) words. A text resource stream reads characters encoded
+ * as UTF-8.
+ *
+ * <note><para>
+ * Thus, the difference between text and binary resources is only important
+ * when opened as a Unicode stream.
+ * </para></note>
+ *
+ * Returns: the new stream, or %NULL.
+ */
+strid_t
+glk_stream_open_resource_uni(glui32 filenum, glui32 rock)
+{
+ /* Adapted from CheapGlk */
+ /* We have been handed an array of bytes. (They're big-endian
+ four-byte chunks, or perhaps a UTF-8 byte sequence, rather than
+ native-endian four-byte integers). So we drop it into str->buffer,
+ rather than str->ubuffer -- we'll have to do the translation in the
+ get() functions. */
+ strid_t str = glk_stream_open_resource(filenum, rock);
+ if(str != NULL)
+ str->unicode = TRUE;
+ return str;
+}
+
/**
* glk_stream_close:
* @str: Stream to close.
IO_WARNING( "Failed to close file", str->filename, g_strerror(errno) );
g_free(str->filename);
break;
+
+ case STREAM_TYPE_RESOURCE:
+ /* Shouldn't free the chunk; someone else might be using it. It will
+ be freed when the resource map is freed. */
+ break;
+
default:
ILLEGAL_PARAM("Unknown stream type: %u", str->type);
return;
{
STREAM_TYPE_WINDOW,
STREAM_TYPE_MEMORY,
- STREAM_TYPE_FILE
+ STREAM_TYPE_FILE,
+ STREAM_TYPE_RESOURCE
};
/**
enum StreamType type;
/* Specific to window stream: the window this stream is connected to */
winid_t window;
- /* For memory and file streams */
+ /* For memory, file, and resource streams */
gboolean unicode;
- /* Specific to memory streams */
- gchar *buffer;
+ /* For file and resource streams */
+ gboolean binary;
+ /* For memory and resource streams */
+ char *buffer;
glui32 *ubuffer;
glui32 mark;
glui32 endmark;
glui32 buflen;
+ /* Specific to memory streams */
gidispatch_rock_t buffer_rock;
/* Specific to file streams */
FILE *file_pointer;
- gboolean binary;
gchar *filename; /* Displayable filename in UTF-8 for error handling */
glui32 lastop; /* 0, filemode_Write, or filemode_Read */
+#include <config.h>
#include "charset.h"
#include "magic.h"
#include "stream.h"
#include <string.h>
#include <glib.h>
#include <glib/gstdio.h>
+#include <glib/gi18n-lib.h>
/* Internal function: ensure that an fseek() is called on a file pointer in
between reading and writing operations, and vice versa. This will only come up
str->write_count += len;
break;
+ case STREAM_TYPE_RESOURCE:
+ ILLEGAL(_("Writing to a resource stream is illegal."));
+ break;
default:
ILLEGAL_PARAM("Unknown stream type: %u", str->type);
}
str->write_count += len;
break;
+ case STREAM_TYPE_RESOURCE:
+ ILLEGAL(_("Writing to a resource stream is illegal."));
+ break;
default:
ILLEGAL_PARAM("Unknown stream type: %u", str->type);
}
return charresult;
}
+/* Internal function: Read one UTF-8 character, which may be more than one byte,
+from a memory stream @str, and return it as a Unicode code point. */
+static glsi32
+read_utf8_char_from_buffer(strid_t str)
+{
+ size_t foo;
+ gunichar charresult = (gunichar)-2;
+ char *buffer = str->buffer + str->mark;
+ size_t maxlen = str->buflen - str->mark;
+
+ if(maxlen == 0)
+ return -1;
+
+ for(foo = 1; foo <= maxlen; foo++)
+ {
+ charresult = g_utf8_get_char_validated(buffer, foo);
+ /* charresult is -1 if invalid, -2 if incomplete, and the
+ Unicode code point otherwise */
+ if(charresult != (gunichar)-2)
+ break;
+ }
+ str->mark += foo;
+ str->read_count++;
+
+ /* Return -1 on EOS */
+ if(charresult == (gunichar)-2)
+ return -1;
+ /* Silently return unknown characters as 0xFFFD, Replacement Character */
+ if(charresult == (gunichar)-1)
+ return 0xFFFD;
+ return charresult;
+}
+
+/* Internal function: Read one big-endian four-byte character from memory and
+return it as a Unicode code point, or -1 on EOF */
+static glsi32
+read_ucs4be_char_from_buffer(strid_t str)
+{
+ glui32 ch = str->buffer[str->mark++];
+ if(str->mark >= str->buflen)
+ return -1;
+ ch = (ch << 8) | (str->buffer[str->mark++] & 0xFF);
+ if(str->mark >= str->buflen)
+ return -1;
+ ch = (ch << 8) | (str->buffer[str->mark++] & 0xFF);
+ if(str->mark >= str->buflen)
+ return -1;
+ ch = (ch << 8) | (str->buffer[str->mark++] & 0xFF);
+ str->read_count++;
+ return ch;
+}
+
/* Internal function: Tell whether this code point is a Unicode newline. The
file pointer and eight-bit flag are included in case the newline is a CR
(U+000D). If the next character is LF (U+000A) then it also belongs to the
{
switch(str->type)
{
+ case STREAM_TYPE_RESOURCE:
+ if(str->unicode)
+ {
+ if(!str->buffer || str->mark >= str->buflen)
+ return -1;
+ if(str->binary)
+ /* Cheap big-endian stream */
+ return read_ucs4be_char_from_buffer(str);
+ /* slightly less cheap UTF8 stream */
+ return read_utf8_char_from_buffer(str);
+ }
+ /* for text streams, fall through to memory case */
case STREAM_TYPE_MEMORY:
if(str->unicode)
{
switch(str->type)
{
+ case STREAM_TYPE_RESOURCE:
+ {
+ int copycount = 0;
+ if(str->unicode)
+ {
+ while(copycount < len && str->buffer && str->mark < str->buflen)
+ {
+ glui32 ch;
+ if(str->binary)
+ ch = read_ucs4be_char_from_buffer(str);
+ else
+ ch = read_utf8_char_from_buffer(str);
+ buf[copycount++] = (ch > 0xFF)? '?' : (char)ch;
+ }
+ return copycount;
+ }
+ /* for text streams, fall through to memory case */
+ }
case STREAM_TYPE_MEMORY:
{
int copycount = 0;
switch(str->type)
{
+ case STREAM_TYPE_RESOURCE:
+ {
+ int copycount = 0;
+ if(str->unicode)
+ {
+ while(copycount < len && str->buffer && str->mark < str->buflen)
+ {
+ glui32 ch;
+ if(str->binary)
+ ch = read_ucs4be_char_from_buffer(str);
+ else
+ ch = read_utf8_char_from_buffer(str);
+ buf[copycount++] = ch;
+ }
+ return copycount;
+ }
+ /* for text streams, fall through to memory case */
+ }
case STREAM_TYPE_MEMORY:
{
int copycount = 0;
switch(str->type)
{
+ case STREAM_TYPE_RESOURCE:
+ {
+ int copycount = 0;
+ if(str->unicode)
+ {
+ /* Do it character-by-character */
+ while(copycount < len - 1 && str->buffer && str->mark < str->buflen)
+ {
+ glsi32 ch;
+ if(str->binary)
+ ch = read_ucs4be_char_from_buffer(str);
+ else
+ ch = read_utf8_char_from_buffer(str);
+ /* Check for Unicode newline; slightly different than
+ in file streams */
+ if(ch == 0x0A || ch == 0x85 || ch == 0x0C || ch == 0x2028 || ch == 0x2029)
+ {
+ buf[copycount++] = '\n';
+ break;
+ }
+ if(ch == 0x0D)
+ {
+ if(str->buffer[str->mark] == 0x0A)
+ str->mark++; /* skip past next newline */
+ buf[copycount++] = '\n';
+ break;
+ }
+ buf[copycount++] = (ch > 0xFF)? '?' : (char)ch;
+ }
+ buf[copycount] = '\0';
+ return copycount;
+ }
+ /* for text streams, fall through to the memory case */
+ }
case STREAM_TYPE_MEMORY:
{
int copycount = 0;
switch(str->type)
{
+ case STREAM_TYPE_RESOURCE:
+ {
+ int copycount = 0;
+ if(str->unicode)
+ {
+ /* Do it character-by-character */
+ while(copycount < len - 1 && str->buffer && str->mark < str->buflen)
+ {
+ glsi32 ch;
+ if(str->binary)
+ ch = read_ucs4be_char_from_buffer(str);
+ else
+ ch = read_utf8_char_from_buffer(str);
+ /* Check for Unicode newline; slightly different than
+ in file streams */
+ if(ch == 0x0A || ch == 0x85 || ch == 0x0C || ch == 0x2028 || ch == 0x2029)
+ {
+ buf[copycount++] = '\n';
+ break;
+ }
+ if(ch == 0x0D)
+ {
+ if(str->ubuffer[str->mark] == 0x0A)
+ str->mark++; /* skip past next newline */
+ buf[copycount++] = '\n';
+ break;
+ }
+ buf[copycount++] = ch;
+ }
+ buf[copycount] = '\0';
+ return copycount;
+ }
+ /* for text streams, fall through to the memory case */
+ }
case STREAM_TYPE_MEMORY:
{
int copycount = 0;
switch(str->type)
{
case STREAM_TYPE_MEMORY:
+ case STREAM_TYPE_RESOURCE:
return str->mark;
case STREAM_TYPE_FILE:
return ftell(str->file_pointer);
switch(str->type)
{
+ case STREAM_TYPE_RESOURCE:
case STREAM_TYPE_MEMORY:
switch(seekmode)
{
* A C
* </literallayout></textobject></mediaobject></entry>
* </row></tbody></tgroup></informaltable>
- * After the first split, the new pair window (O1, which covers the whole
- * screen) knows that its first child (A) is above the second, and gets 50% of
- * its own area. (A is the key window for this split, but a proportional split
- * doesn't care about key windows.)
+ * The initial window is A. After the first split, the new pair window (O1,
+ * which covers the whole screen) knows that its new child (B) is below A, and
+ * gets 50% of its own area. (B is the key window for this split, but a
+ * proportional split doesn't care about key windows.)
*
- * After the second split, all this remains true; O1 knows that its first child
- * gets 50% of its space, and A is O1's key window. But now O1's first child is
- * O2 instead of A. The newer pair window (O2) knows that its first child (C)
- * is above the second, and gets a fixed size of two rows. (As measured in C's
- * font, because C is O2's key window.)
+ * After the <emphasis>second</emphasis> split, all this remains true; O1 knows
+ * that its first child gets 50% of its space, and B is O1's key window. But
+ * now O1's first child is O2 instead of A. The newer pair window (O2) knows
+ * that its first child (C) is above the second, and gets a fixed size of two
+ * rows. (As measured in C's font, because C is O2's key window.)
*
* If we split C, now, the resulting pair will still be two C-font rows high
* — that is, tall enough for two lines of whatever font C displays. For
endif
-EXTRA_DIST = $(gsettings_SCHEMAS)
+CLEANFILES = config.pyc
+DISTCLEANFILES = config.py
+
+EXTRA_DIST = \
+ $(gsettings_SCHEMAS) \
+ config.py \
+ player.py
+
-include $(top_srcdir)/git.mk
--- /dev/null
+<?xml version="1.0"?>
+<ui>
+ <menubar>
+ <menu action="game">
+ <menuitem action="open"/>
+ <menuitem action="recent"/>
+ <separator/>
+ <menuitem action="stop"/>
+ <menuitem action="quit_chimara"/>
+ </menu>
+ <menu action="edit">
+ <menuitem action="copy"/>
+ <menuitem action="paste"/>
+ <separator/>
+ <menuitem action="preferences"/>
+ </menu>
+ <menu action="view">
+ <menuitem action="toolbar"/>
+ </menu>
+ <menu action="command">
+ <menuitem action="undo"/>
+ <menuitem action="save"/>
+ <menuitem action="restore"/>
+ <menuitem action="restart"/>
+ <menuitem action="quit"/>
+ </menu>
+ <menu action="help">
+ <menuitem action="about"/>
+ </menu>
+ </menubar>
+ <toolbar>
+ <toolitem action="open"/>
+ <separator/>
+ <toolitem action="restore"/>
+ <toolitem action="save"/>
+ </toolbar>
+</ui>
+++ /dev/null
-<?xml version="1.0"?>
-<ui>
- <menubar>
- <menu action="game">
- <menuitem action="open"/>
- @OPEN_RECENT_MENU_ITEM@
- <separator/>
- <menuitem action="stop"/>
- <menuitem action="quit_chimara"/>
- </menu>
- <menu action="edit">
- <menuitem action="copy"/>
- <menuitem action="paste"/>
- <separator/>
- <menuitem action="preferences"/>
- </menu>
- <menu action="view">
- <menuitem action="toolbar"/>
- </menu>
- <menu action="command">
- <menuitem action="undo"/>
- <menuitem action="save"/>
- <menuitem action="restore"/>
- <menuitem action="restart"/>
- <menuitem action="quit"/>
- </menu>
- <menu action="help">
- <menuitem action="about"/>
- </menu>
- </menubar>
- <toolbar>
- <toolitem action="open"/>
- <separator/>
- <toolitem action="restore"/>
- <toolitem action="save"/>
- </toolbar>
-</ui>
--- /dev/null
+PACKAGE_VERSION = '''@PACKAGE_VERSION@'''
g_free(title);
}
-static void
+static gboolean
create_window(void)
{
GError *error = NULL;
error = NULL;
if( !gtk_builder_add_from_file(builder, PACKAGE_SRC_DIR "/chimara.ui", &error) ) {
#endif /* DEBUG */
- error_dialog(NULL, error, "Error while building interface: ");
- return;
+ return FALSE;
#ifdef DEBUG
}
#endif /* DEBUG */
#ifdef DEBUG
g_error_free(error);
error = NULL;
- if( !gtk_ui_manager_add_ui_from_file(uimanager, PACKAGE_SRC_DIR "/chimara.menus", &error) ) {
-#endif /* DEBUG */
- error_dialog(NULL, error, "Error while building interface: ");
- return;
-#ifdef DEBUG
- }
+ if( !gtk_ui_manager_add_ui_from_file(uimanager, PACKAGE_SRC_DIR "/chimara.menus", &error) )
#endif /* DEBUG */
+ return FALSE;
}
glk = chimara_if_new();
#ifdef DEBUG
g_error_free(error);
error = NULL;
- if( !chimara_glk_set_css_from_file(CHIMARA_GLK(glk), PACKAGE_SRC_DIR "/style.css", &error) ) {
-#endif /* DEBUG */
- error_dialog(NULL, error, "Couldn't open CSS file: ");
- return;
-#ifdef DEBUG
- }
+ if( !chimara_glk_set_css_from_file(CHIMARA_GLK(glk), PACKAGE_SRC_DIR "/style.css", &error) )
#endif /* DEBUG */
+ return FALSE;
}
/* DON'T UNCOMMENT THIS your eyes will burn
GtkBox *vbox = GTK_BOX( gtk_builder_get_object(builder, "vbox") );
if(vbox == NULL)
- {
- error_dialog(NULL, NULL, "Could not find vbox");
- return;
- }
+ return FALSE;
gtk_ui_manager_insert_action_group(uimanager, actiongroup, 0);
GtkWidget *menubar = gtk_ui_manager_get_widget(uimanager, "/menubar");
/* Create preferences window */
preferences_create(CHIMARA_GLK(glk));
+
+ return TRUE;
}
int
state_settings = g_settings_new_with_backend("org.chimara-if.player.state", backend);
g_free(keyfile);
- create_window();
+ if( !create_window() ) {
+ error_dialog(NULL, NULL, "Error while building interface.");
+ return 1;
+ }
gtk_widget_show_all(window);
g_object_unref( G_OBJECT(uimanager) );
--- /dev/null
+#!/usr/bin/env python
+
+import sys
+import os.path
+from gi.repository import GObject, GLib, Gdk, Gio, Gtk, Chimara
+import config
+
+# FIXME: Dummy translation function, for now
+_ = lambda x: x
+
+
+class Player(GObject.GObject):
+ __gtype_name__ = 'ChimaraPlayer'
+
+ def __init__(self):
+ super(Player, self).__init__()
+
+ # FIXME: should use the Keyfile backend, but that's not available from
+ # Python
+ self.prefs_settings = Gio.Settings('org.chimara-if.player.preferences')
+ self.state_settings = Gio.Settings('org.chimara-if.player.state')
+
+ builder = Gtk.Builder()
+ builder.add_from_file('chimara.ui')
+ self.window = builder.get_object('chimara')
+ self.aboutwindow = builder.get_object('aboutwindow')
+ self.prefswindow = builder.get_object('prefswindow')
+ actiongroup = builder.get_object('actiongroup')
+
+ # Set the default value of the "View/Toolbar" menu item upon creation
+ # of a new window to the "show-toolbar-default" setting, but bind the
+ # setting one-way only - we don't want toolbars to disappear suddenly
+ toolbar_action = builder.get_object('toolbar')
+ toolbar_action.active = \
+ self.state_settings.get_boolean('show-toolbar-default')
+ self.state_settings.bind('show-toolbar-default', toolbar_action,
+ 'active', Gio.SettingsBindFlags.SET)
+
+ filt = Gtk.RecentFilter()
+ for pattern in ['*.z[1-8]', '*.[zg]lb', '*.[zg]blorb', '*.ulx', '*.blb',
+ '*.blorb']:
+ filt.add_pattern(pattern)
+ recent = builder.get_object('recent')
+ recent.add_filter(filt)
+
+ uimanager = Gtk.UIManager()
+ uimanager.add_ui_from_file('chimara.menus')
+ uimanager.insert_action_group(actiongroup, 0)
+ menubar = uimanager.get_widget('/menubar')
+ toolbar = uimanager.get_widget('/toolbar')
+ toolbar.no_show_all = True
+ if toolbar_action.active:
+ toolbar.show()
+ else:
+ toolbar.hide()
+
+ # Connect the accelerators
+ accels = uimanager.get_accel_group()
+ self.window.add_accel_group(accels)
+
+ self.glk = Chimara.IF()
+ self.glk.props.ignore_errors = True
+ self.glk.set_css_from_file('style.css')
+
+ vbox = builder.get_object('vbox')
+ vbox.pack_end(self.glk, True, True, 0)
+ vbox.pack_start(menubar, False, False, 0)
+ vbox.pack_start(toolbar, False, False, 0)
+
+ #builder.connect_signals(self) # FIXME Segfaults?!
+ builder.get_object('open').connect('activate', self.on_open_activate)
+ builder.get_object('restore').connect('activate',
+ self.on_restore_activate)
+ builder.get_object('save').connect('activate', self.on_save_activate)
+ builder.get_object('stop').connect('activate', self.on_stop_activate)
+ builder.get_object('recent').connect('item-activated',
+ self.on_recent_item_activated)
+ builder.get_object('undo').connect('activate', self.on_undo_activate)
+ builder.get_object('quit').connect('activate', self.on_quit_activate)
+ builder.get_object('copy').connect('activate', self.on_copy_activate)
+ builder.get_object('paste').connect('activate', self.on_paste_activate)
+ builder.get_object('preferences').connect('activate',
+ self.on_preferences_activate)
+ builder.get_object('about').connect('activate', self.on_about_activate)
+ toolbar_action.connect('toggled', self.on_toolbar_toggled)
+ self.aboutwindow.connect('response', lambda x, *args: x.hide())
+ self.aboutwindow.connect('delete-event',
+ lambda x, *args: x.hide_on_delete())
+ self.window.connect('delete-event', self.on_window_delete_event)
+ self.prefswindow.connect('response', lambda x, *args: x.hide())
+ self.prefswindow.connect('delete-event',
+ lambda x, *args: x.hide_on_delete())
+ # FIXME Delete to here when above bug is fixed
+
+ self.glk.connect('notify::program-name', self.change_window_title)
+ self.glk.connect('notify::story-name', self.change_window_title)
+
+ # Create preferences window
+ # TODO
+
+ def change_window_title(self, glk, pspec, data=None):
+ if glk.props.program_name is None:
+ title = "Chimara"
+ elif glk.props.story_name is None:
+ title = "{interp} - Chimara".format(interp=glk.props.program_name)
+ else:
+ title = "{interp} - {story} - Chimara".format(
+ interp=glk.props.program_name,
+ story=glk.props.story_name)
+ self.window.props.title = title
+
+ def on_open_activate(self, action, data=None):
+ if not self.confirm_open_new_game():
+ return
+
+ dialog = Gtk.FileChooserDialog(_('Open Game'), self.window,
+ Gtk.FileChooserAction.OPEN,
+ (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
+ Gtk.STOCK_OPEN, Gtk.ResponseType.ACCEPT))
+
+ # Get last opened path
+ path = _maybe(self.state_settings.get_value('last-open-path'))
+ if path is not None:
+ dialog.set_current_folder(path)
+
+ response = dialog.run()
+ dialog.hide()
+ if response != Gtk.ResponseType.ACCEPT:
+ return
+
+ gamefile = dialog.get_file()
+ self.search_for_graphics_file(gamefile.get_path())
+ try:
+ self.glk.run_game_file(gamefile)
+ except GLib.Error as e:
+ error_dialog(self.window, _('Could not open game file {filename}: {errmsg}').format(filename=gamefile.get_path(), errmsg=e.message))
+ return
+
+ path = dialog.get_current_folder()
+ if path is not None:
+ self.state_settings.last_open_path = path
+
+ # Add file to recent files list
+ manager = Gtk.RecentManager.get_default()
+ uri = gamefile.get_uri()
+ manager.add_item(uri)
+
+ dialog.destroy()
+
+ def on_recent_item_activated(self, chooser, data=None):
+ if not self.confirm_open_new_game():
+ return
+
+ uri = chooser.get_current_uri()
+ gamefile = Gio.file_new_for_uri(uri)
+
+ self.search_for_graphics_file(gamefile.get_path())
+ try:
+ self.glk.run_game_file(gamefile)
+ except GLib.Error as e:
+ error_dialog(self.window,
+ _('Could not open game file {filename}: {errmsg}').format(
+ filename=gamefile.get_basename(),
+ errmsg=e.message))
+ return
+
+ # Add file to recent files list again, this updates it to most recently
+ # used
+ manager = Gtk.RecentManager.get_default()
+ manager.add_item(uri)
+
+ def on_stop_activate(self, action, data=None):
+ self.glk.stop()
+
+ def on_quit_chimara_activate(self, action, data=None):
+ Gtk.main_quit()
+
+ def on_copy_activate(self, action, data=None):
+ focus = self.window.get_focus()
+ # Call "copy clipboard" on any widget that defines it
+ if (isinstance(focus, Gtk.Label)
+ or isinstance(focus, Gtk.Entry)
+ or isinstance(focus, Gtk.TextView)):
+ focus.emit('copy-clipboard')
+
+ def on_paste_activate(self, action, data=None):
+ focus = self.window.get_focus()
+ # Call "paste clipboard" on any widget that defines it
+ if isinstance(focus, Gtk.Entry) or isinstance(focus, Gtk.TextView):
+ focus.emit('paste-clipboard')
+
+ def on_preferences_activate(self, action, data=None):
+ self.prefswindow.present()
+
+ def on_toolbar_toggled(self, action, data=None):
+ if action.get_active():
+ self.toolbar.show()
+ else:
+ self.toolbar.hide()
+
+ def on_undo_activate(self, action, data=None):
+ self.glk.feed_line_input('undo')
+
+ def on_save_activate(self, action, data=None):
+ self.glk.feed_line_input('save')
+
+ def on_restore_activate(self, action, data=None):
+ self.glk.feed_line_input('restore')
+
+ def on_restart_activate(self, action, data=None):
+ self.glk.feed_line_input('restart')
+
+ def on_quit_activate(self, action, data=None):
+ self.glk.feed_line_input('quit')
+
+ def on_about_activate(self, action, data=None):
+ self.aboutwindow.set_version(config.PACKAGE_VERSION)
+ self.aboutwindow.present()
+
+ def on_window_delete_event(self, widget, event, data=None):
+ Gtk.main_quit()
+ return True
+
+ def confirm_open_new_game(self):
+ """
+ If a game is running in the Glk widget, warn the user that they will
+ quit the currently running game if they open a new one. Returns True if
+ no game was running. Returns False if the user cancelled. Returns True
+ and shuts down the running game if the user wishes to continue.
+ """
+ if not self.glk.props.running:
+ return True
+
+ dialog = Gtk.MessageDialog(self.window,
+ Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
+ Gtk.MessageType.WARNING, Gtk.ButtonsType.CANCEL,
+ _("Are you sure you want to open a new game?"))
+ dialog.format_secondary_text(
+ _("If you open a new game, you will quit the one you are "
+ "currently playing."))
+ dialog.add_button(Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
+ response = dialog.run()
+ dialog.hide()
+
+ if response != Gtk.ResponseType.OK:
+ return False
+
+ self.glk.stop()
+ self.glk.wait()
+ return True
+
+ def search_for_graphics_file(self, filename):
+ """Internal function: See if there is a corresponding graphics file"""
+
+ # First get the name of the story file
+ base = os.path.basename(filename)
+ base_noext = os.path.splitext(base)[0]
+
+ # Check in the stored resource path, if set
+ resource_path = _maybe(self.prefs_settings.get_value('resource-path'))
+
+ # Otherwise check in the current directory
+ if resource_path is None:
+ resource_path = os.path.dirname(filename)
+
+ blorbfile = os.path.join(resource_path, base_noext + '.blb')
+ if os.path.exists(blorbfile):
+ self.glk.graphics_file = blorbfile
+
+
+def _maybe(variant):
+ """Gets a maybe value from a GVariant - not handled in PyGI"""
+ v = variant.get_maybe()
+ if v is None:
+ return None
+ return v.unpack()
+
+
+def error_dialog(parent, message):
+ dialog = Gtk.MessageDialog(parent, Gtk.DialogFlags.DESTROY_WITH_PARENT,
+ Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, message)
+ dialog.run()
+ dialog.destroy()
+
+if __name__ == '__main__':
+ Gdk.threads_init()
+
+ player = Player()
+ player.window.show_all()
+
+ if len(sys.argv) == 3:
+ player.glk.props.graphics_file = sys.argv[2]
+ if len(sys.argv) >= 2:
+ try:
+ player.glk.run_game(sys.argv[1])
+ except GLib.Error as e:
+ error_dialog(player.window,
+ _("Error starting Glk library: {errmsg}").format(
+ errmsg=e.message))
+ sys.exit(1)
+
+ Gdk.threads_enter()
+ Gtk.main()
+ Gdk.threads_leave()
+
+ player.glk.stop()
+ player.glk.wait()
+
+ sys.exit(0)
<?xml version="1.0"?>
<interface>
- <!-- interface-requires gtk+ 2.12 -->
+ <requires lib="gtk+" version="2.16"/>
<!-- interface-naming-policy project-wide -->
<object class="GtkWindow" id="window">
<property name="border_width">6</property>
<row>
<col id="0" translatable="yes">ArrayLimitTest</col>
<col id="1">arraylimittest.ulx</col>
+ </row>
+ <row>
+ <col id="0" translatable="yes">ChineseDictTest</col>
+ <col id="1">chinesedicttest.ulx</col>
</row>
<row>
<col id="0" translatable="yes">DateTimeTest</col>
<col id="1">datetimetest.ulx</col>
</row>
+ <row>
+ <col id="0" translatable="yes">DictFlagTest</col>
+ <col id="1">dictflagtest.ulx</col>
+ </row>
+ <row>
+ <col id="0" translatable="yes">DictFlagTest (Z5)</col>
+ <col id="1">dictflagtest.z5</col>
+ </row>
<row>
<col id="0" translatable="yes">ExternalFile</col>
<col id="1">externalfile.ulx</col>
<col id="0" translatable="yes">MemStreamTest</col>
<col id="1">memstreamtest.ulx</col>
</row>
+ <row>
+ <col id="0" translatable="yes">ResStreamTest</col>
+ <col id="1">resstreamtest.gblorb</col>
+ </row>
<row>
<col id="0" translatable="yes">UniCaseTest</col>
<col id="1">unicasetest.ulx</col>
<col id="0" translatable="yes">UniDictTest</col>
<col id="1">unidicttest.ulx</col>
</row>
+ <row>
+ <col id="0" translatable="yes">UniSourceTest</col>
+ <col id="1">unisourcetest.ulx</col>
+ </row>
<row>
<col id="0" translatable="yes">WindowTest</col>
<col id="1">windowtest.ulx</col>
#include <unistd.h>
#include <string.h>
+#define NUM_CHANNELS 2
+
void
glk_main(void)
{
- if(!glk_gestalt(gestalt_Sound, 0)) {
+ if(!glk_gestalt(gestalt_Sound2, 0)) {
fprintf(stderr, "Sound not supported.\n");
return;
}
- if(!glk_gestalt(gestalt_SoundVolume, 0)) {
- fprintf(stderr, "Sound volume not supported.\n");
- return;
- }
- if(!glk_gestalt(gestalt_SoundNotify, 0)) {
- fprintf(stderr, "Sound notification not supported.\n");
- return;
- }
- schanid_t sc = glk_schannel_create(0);
- if(!sc) {
- fprintf(stderr, "Could not create sound channel.\n");
- return;
+ schanid_t sc[NUM_CHANNELS];
+ int count;
+ for(count = 0; count < NUM_CHANNELS; count++) {
+ sc[count] = glk_schannel_create(count);
+ if(!sc[count]) {
+ fprintf(stderr, "Could not create sound channel number %d.\n", count);
+ return;
+ }
}
/* Open the main window. */
glk_set_window(mainwin);
glk_put_string("Copy a sound file to the current directory and rename it "
"to SND3. Supported formats: AIFF, OGG, MOD, S3M, IT, XM. Type 'play' "
- "to play it.\n");
+ "to play it.\n\n"
+ "If you want to test multi-sound playing, copy another sound file and "
+ "rename it to SND4 as well. You can't stop it, so make it a short "
+ "sound effect.\n");
char buffer[1024];
int len;
int finish = 0;
int repeat = 1;
+ int ramp = 0;
event_t ev;
while(!finish) {
finish = 1;
} else if(strcmp(buffer, "play") == 0) {
glk_put_string("Playing sound.\n");
- if(!glk_schannel_play_ext(sc, 3, repeat, 1)) {
+ if(!glk_schannel_play_ext(sc[0], 3, repeat, 1)) {
fprintf(stderr, "Could not start sound channel.\n");
finish = 1;
}
} else if(strcmp(buffer, "stop") == 0) {
glk_put_string("Stopping sound.\n");
- glk_schannel_stop(sc);
+ glk_schannel_stop(sc[0]);
} else if(strcmp(buffer, "repeat") == 0) {
glk_put_string("Setting repeat to ");
if(repeat == 1) {
glk_put_string("ONCE.\n");
repeat = 1;
}
+ } else if(strcmp(buffer, "pause") == 0) {
+ glk_put_string("Pausing channel.\n");
+ glk_schannel_pause(sc[0]);
+ } else if(strcmp(buffer, "unpause") == 0) {
+ glk_put_string("Unpausing channel.\n");
+ glk_schannel_unpause(sc[0]);
+ } else if(strcmp(buffer, "ramp") == 0) {
+ glk_put_string("Ramping volume to ");
+ if(ramp == 0) {
+ glk_put_string("HALF.\n");
+ glk_schannel_set_volume_ext(sc[0], 0x8000, 3000, 42);
+ ramp = 1;
+ } else if(ramp == 1) {
+ glk_put_string("FULL.\n");
+ glk_schannel_set_volume_ext(sc[0], 0x10000, 3000, 69);
+ ramp = 0;
+ }
+ } else if(strcmp(buffer, "multi") == 0) {
+ glk_put_string("Playing two sounds. (These will not repeat.)\n");
+ glui32 sounds[NUM_CHANNELS] = { 3, 4 };
+ if(glk_schannel_play_multi(sc, NUM_CHANNELS, sounds, NUM_CHANNELS, 1) < 2) {
+ fprintf(stderr, "Tried to start %d sounds, but not all were successful.", NUM_CHANNELS);
+ finish = 1;
+ }
} else if(strcmp(buffer, "help") == 0) {
- glk_put_string("Type PLAY or REPEAT or STOP or QUIT.\n");
+ glk_put_string("Type PLAY or MULTI or REPEAT or PAUSE or UNPAUSE or RAMP or STOP or QUIT.\n");
}
break;
case evtype_SoundNotify:
glk_cancel_line_event(mainwin, NULL);
glk_put_string("\nGot sound notify event!\n");
break;
+ case evtype_VolumeNotify:
+ glk_cancel_line_event(mainwin, NULL);
+ glk_put_string("\nGot volume notify event!\n");
+ break;
default:
;
}
}
- glk_schannel_stop(sc);
- glk_schannel_destroy(sc);
+ for(count = 0; count < NUM_CHANNELS; count++) {
+ glk_schannel_stop(sc[count]);
+ glk_schannel_destroy(sc[count]);
+ }
}