REM A module to make sound effects on system events : REM This is the amount of data allocated for each event; since we play all REM our samples from disc, and from one folder, this needs only contain REM space for the leafname of each sample, i.e. 10 characters + a zero byte. REM REM This is changeable so we could adapt the module to play some samples REM from memory (hint, hint) event_len=11 : REM Number of events num_events=10 REM last_event is the offset in the workspace pointing to the last event's REM filename; add event_len to this value to get the size of the workspace last_event=(num_events-1)*event_len : REM Events list, in descending order of priority; sounds further up in the REM list will override sounds below that are already playing. Remember to REM mirror this list in the text at the end of the module. event_desktopstart=0 event_shutdown=1 event_wimperrorbox=2 event_screenblank=3 event_screenrestore=4 event_wimpsave=5 event_modechange=6 event_wimpinitialise=7 event_wimpclosedown=8 event_wimpmenuselect=9 : ON ERROR E$=REPORT$+" at line "+STR$(ERL):OFF:ERROR 0,E$ : DIM code 4096 FOR pass=12 TO 14 STEP 2 P%=0:O%=code:L%=code+4096 [OPT pass EQUD 0 ; 'Start' code EQUD initialise EQUD finalise EQUD service_calls EQUD title EQUD help EQUD command_table .title EQUS "NoiseBox":EQUB 0 .help EQUS "NoiseBox":EQUB 9:EQUB 9:EQUS "0.01 (30 Mar 1997)":EQUB 0 .syntax_nbassign EQUS "Syntax: *NoiseAssign [system event] [sound filename]":EQUB0 .help_nbassign FNinsert_binary(".NA_Help") ; A macro! EQUB 0 .commas EQUS "event,sound":EQUB 0 .assigned_to EQUS " assigned to ":EQUB 0 ALIGN .error_badevent EQUD 0:EQUS "Event name not recongised. Type *NoiseAssign without any parameters to see a list of event names.":EQUB 0 ALIGN .error_nametoolong EQUD 0 EQUS "Sound filename is too long; should be ten letters or less." EQUB 0 : ALIGN .initialise STMFD R13!,{R14} : MOV R0,#6 MOV R3,#last_event+event_len SWI "XOS_Module" ; Claim some workspace LDMVSFD R13!,{R0-R5,PC} STR R2,[R12] ; Store it in the module's private word MOV R12,R2 ; R12 (now) = real workspace : MOV R0,#0 ; Set workspace to zeroes .initialise_zero_workspace_loop STRB R0,[R2,R3] SUBS R3,R3,#1 BNE initialise_zero_workspace_loop : ADR R0,title ; Filter title ADR R1,wimp_poll_trap ; Filter code MOV R2,R12 ; Address to pass in R12 MOV R3,#0 ; 0 = applicable to all tasks LDR R4,mask ; Event mask (all except 9,17-19) SWI "XFilter_RegisterPostFilter" ; Install filter : LDMFD R13!,{PC}^ : .finalise LDR R12,[R12] ; R12 > real workspace STMFD R13!,{R14} : ADR R0,title ADR R1,wimp_poll_trap MOV R2,R12 MOV R3,#0 LDR R4,mask SWI "XFilter_DeRegisterPostFilter" ; Remove filter before quit : LDMFD R13!,{PC} : .mask EQUD %11100101110011 : .service_errorstarting EQUD &400C0 .service_calls ; R1 = service call number ; ; Acorn say we ought to pass unwanted service calls on ASAP, so there's some ; code bloating here. Note how to do an OR comparison in ARM code... ; STMFD R13!,{R0,R14} CMP R1,#&46 ; ModeChange CMPNE R1,#&7A ; ScreenBlank CMPNE R1,#&7B ; ScreenRestore CMPNE R1,#&7C ; DesktopStart CMPNE R1,#&80 ; Shutdown LDRNE R0,service_errorstarting CMPNE R1,R0 LDMFD R13!,{R0,R14} MOVNES PC,R14 : LDR R12,[R12] ; Get the workspace pointer STMFD R13!,{R0,R8-R9,R14} : MOV R9,PC ; Could be in IRQ or SVC mode, so we ORR R8,R9,#3 ; have to change SVC just in case... TEQP R8,#0 MOV R0,R0 : CMP R1,#&46 MOVEQ R0,#event_modechange BLEQ make_noise CMP R1,#&7A MOVEQ R0,#event_screenblank BLEQ make_noise CMP R1,#&7B MOVEQ R0,#event_screenrestore BLEQ make_noise CMP R1,#&7C MOVEQ R0,#event_desktopstart BLEQ make_noise CMP R1,#&80 MOVEQ R0,#event_shutdown BLEQ make_noise LDR R0,service_errorstarting CMP R1,R0 MOVEQ R0,#event_wimperrorbox BLEQ make_noise : TEQP R9,#0 MOV R0,R0 ; Restore processor mode : LDMFD R13!,{R0,R8-R9,PC}^ : ; ; Wimp_ SWI trapping routines; these just make the approriate noise, then ; return. Code is always entered in SVC mode, so we don't need the above ; messing around. ; .wimp_poll_trap ; ; This code is called before the WIMP returns to a task with Wimp_Poll; the ; event code is passed in R0 and the message block pointer in R1 ; STMFD R13!,{R0,R3-R4,R14} : CMP R0,#9 ; Menu select MOVEQ R0,#event_wimpmenuselect BLEQ make_noise : CMP R0,#17 CMPNE R0,#18 CMPNE R0,#19 BNE not_wimp_sendmessage : LDR R3,[R1,#16] CMP R3,#1 MOVEQ R0,#event_wimpsave BLEQ make_noise : MOV R4,#&40000 ADD R4,R4,#&C2 CMP R3,R4 MOVEQ R0,#event_wimpinitialise BLEQ make_noise : MOV R4,#&40000 ADD R4,R4,#&C3 CMP R3,R4 MOVEQ R0,#event_wimpclosedown BLEQ make_noise : .not_wimp_sendmessage LDMFD R13!,{R0,R3-R4,PC}^ : .command_table EQUS "NoiseAssign":EQUB 0:ALIGN ; * command to implement EQUD command_nbassign ; Offset to code EQUB 0:EQUB 0:EQUB 2:EQUB 0 ; Min, ---, Max, --- EQUD syntax_nbassign:EQUD help_nbassign EQUD 0 ; Terminate command table : .command_nbassign LDR R12,[R12] ; Workspace pointer in R12 STMFD R13!,{R14} ; Entered with R0 > command tail ; and R1 = no. of parameters CMP R1,#0 ; No parameters?... BEQ command_nbassign_listall : ; ...Otherwise we're assigning a sound to an event : MOV R1,R0 FNadrl (0,commas) ; Watch for these macros... ADR R2,scratch SWI "XOS_ReadArgs" ; Look it up! LDMVSFD R13!,{PC} ; Return if there's an error (V set) MOV R4,R2 ; R4 > output buffer (we need R2...) : ADR R3,event_text_start ; R3 > list of event names MOV R5,#0 ; R5 = event number (counts up...) : ; ; The following piece of code tries to match the event name entered by the ; user to an event number; the events are listed in order at the end of the ; program. It shows some of the difficulties of string handling in ARM code. ; .command_nbassign_compare_startagain LDR R2,[R4] ; R2 > event name as entered by user .command_nbassign_compare_loop LDRB R0,[R2],#1 ; Get byte from user input LDRB R1,[R3],#1 ; Get byte from our list (below) CMP R0,#0 ; If both pointers hit a zero-byte at CMPEQ R1,#0 ; once, we've got a match (R5 = event) BEQ command_nbassign_compare_match ; So jump away... CMP R0,R1 ; Keep comparing bytes until the two ; bytes don't match BEQ command_nbassign_compare_loop ; ; If we get here, it means two bytes haven't matched ; .command_nbassign_compare_next_zero LDRB R1,[R3],#1 ; Skip to start of next string in list CMP R1,#0 BNE command_nbassign_compare_next_zero LDRB R1,[R3] CMP R1,#0 ADDNE R5,R5,#1 BNE command_nbassign_compare_startagain FNadrl (0,error_badevent) LDMFD R13!,{R14} ORRS PC,R14,#1<<28 .command_nbassign_compare_match ; R5 (now) = event number : LDR R2,[R4,#4] ; R2 > sound filename MOV R3,#event_len ; R3 = space allocated per event in ; the module workspace (i.e. ; the filename) MLA R3,R5,R3,R12 ; R3 = (R5 * R3) + R12 : CMP R2,#0 ; If we don't have a second parameter STREQB R2,[R3] ; from the user, we store a zero-byte LDMEQFD R13!,{PC} ; and exit straight away. .command_nbassign_copy_filename_loop ; Quick loop to copy a string... LDRB R0,[R2],#1 STRB R0,[R3],#1 CMP R0,#0 BNE command_nbassign_copy_filename_loop : LDMFD R13!,{PC} : .command_nbassign_listall ; List all events and associated sounds ADR R3,event_text_start MOV R4,R12 ADD R5,R4,#last_event .command_nbassign_listall_loop .command_nbassign_listall_print_word_loop ; ; Print the event names one letter at a time since they're all stored in a list ; and it saves counting string lengths. A good example of when a program's ; speed can be sacrificed to save working out about ten lines of code ;-) ; LDRB R0,[R3],#1 CMP R0,#0 SWINE "OS_WriteC" BNE command_nbassign_listall_print_word_loop FNadrl (0,assigned_to) ; (label is up at the top!) SWI "OS_Write0" MOV R0,R4 SWI "OS_Write0" SWI "OS_NewLine" ADD R4,R4,#event_len CMP R4,R5 BLE command_nbassign_listall_loop LDMFD R13!,{PC} : .make_noise ; R0 = event number ; R12 > workspace STMFD R13!,{R0-R2,R14} ; ; This routine is called to make a noise for a particular event. It doesn't ; actually start the sample player; instead it plays the sample by means of ; a callback handler. This is a routine called by RISC OS as soon as it is ; engaged in something less critical (whenever the processor is back in user ; mode with interrupts enabled, in fact). See the magazine for more details ; STR R0,next_event_number ADR R0,make_noise_callback MOV R1,R12 ; Value to pass in R12 when ; the callback is called SWI "XOS_AddCallBack" LDMFD R13!,{R0-R2,PC}^ : .next_event_number EQUD 0 .last_event_number EQUD 0 : ; ; This routine gets called as soon as event has occurred, but since it is ; through a callback, the OS waits until the processor is engaged in something ; less critical (i.e. whenever it is back in user mode). The time difference ; between the event happening and the sound being played is negligible. ; ; I've stacked all the registers during this routine because it didn't ; seem to work if I just stacked that ones that *should* get corrupted. ; .make_noise_callback STMFD R13!,{R0-R12,R14} : LDR R3,next_event_number ; Get previously stored event MOV R1,#event_len MLA R0,R1,R3,R12 ; R0 > sound filename LDRB R2,[R0] CMP R2,#0 ; Check event isn't disabled LDMEQFD R13!,{R0-R12,PC}^ ; (return if it is) ADR R1,scratch .make_noise_callback_copy_loop LDRB R2,[R0],#1 ; String copy loop again... STRB R2,[R1],#1 CMP R2,#0 BNE make_noise_callback_copy_loop : SWI "XPlaySample_Status" ; Make sure another sample TST R0,#1 ; isn't playing BEQ make_noise_callback_silence LDR R2,last_event_number CMP R2,R3 ; Higher priority event? LDMGTFD R13!,{R0-R12,PC}^ ; Return (don't play new) sound if not .make_noise_callback_silence STR R3,last_event_number ADR R0,sample_filename MVN R1,#0 ; R1 = (NOT 0) = -1 MOV R2,#0 MOV R3,#0 MOV R4,#0 MOV R5,#0 SWI "XPlaySample_Open" ; Open an Armadeus file... SWI "XPlaySample_Play" ; ...and play it LDMFD R13!,{R0-R12,PC}^ : ; ; The event list; this is searched through whenever the user enters an ; event name to assign a sample to. The first one in the list is event 0, the ; next is 1... I'll leave the rest as an exercise for the reader ;-) ; ; This list should mirror the event no. list at the top of the program; the ; noise made by the top event in the list will override any already playing ; lower down. ; .event_text_start EQUS "DesktopStart":EQUB 0 ; SVC &7C EQUS "Shutdown":EQUB 0 ; SVC &80 EQUS "WimpErrorBox":EQUB 0 ; SVC &400C0 (Wimp_ReportError) EQUS "ScreenBlank":EQUB 0 ; SVC &7A EQUS "ScreenRestore":EQUB 0 ; SVC &7B EQUS "WimpSave":EQUB 0 ; DataSave msg. EQUS "ModeChange":EQUB 0 ; SVC &46 EQUS "WimpInitialise":EQUB 0 ; TaskInitialise msg. EQUS "WimpCloseDown":EQUB 0 ; TaskCloseDown msg. EQUS "WimpMenuSelect":EQUB 0 ; Wimp_Poll, event=9 EQUB 0 ; Terminate with a zero ALIGN : ; ; Our scratch space is used to store the leafname of each sample to be played ; but it also has to be word-aligned to be used as an output buffer for ; OS_ReadArgs... hence the two padding bytes here so that the pathname ends ; on a word boundary. ; EQUB0:EQUB0 .sample_filename EQUS ".Noises." ; 24 letters => word aligned .scratch FNfill_space(128) : ]:NEXT REM Save our module out with the correct filetype SYS "OS_File",10,".NoiseBox",&FFA,,code,O% END : DEF FNinsert_binary(f$):LOCAL l% REM Inserts a binary into our module; a trivial example, but it shows how REM one uses macros to save on messy code. REM REM In fact, all it does is to load the file wherever O% happens to be, then REM advances O% and P% by the file's length. IF pass=14 THEN REM Only actually *load* the file on the second code pass... SYS "OS_File",16,f$,O%,0 TO ,,,,l% ELSE REM ...on the first, we just need to leave space for it. SYS "OS_File",23,f$ TO ,,,,l% ENDIF O%+=l%:P%+=l% =0 : DEF FNfill_space(b) REM Another macro to leave some fresh air in our code O%+=b:P%+=b =0 : DEF FNadrl(register, addr%):LOCAL offset,subtract REM A macro to assemble a 'long' ADR instruction; because of its reliance REM on immediate constants, the ADR directive (which assembles to something REM like ADD R0,PC,#constant) needs two or more ADD or SUB instructions to REM reach the address it needs to. Hence if you get a 'bad immediate constant' REM when assembling an ADR, this is a good macro to use offset=addr%-(P%+8) IF offset<0 THEN subtract=TRUE:offset=-offset ELSE subtract=FALSE IF subtract THEN [OPT pass SUB register,PC,#offset AND &FF SUB register,register,#offset AND &FF00 ] ELSE [OPT pass ADD register,PC,#offset AND &FF ADD register,register,#offset AND &FF00 ] ENDIF =0