#if 0 gcc ${CFLAGS:--s -O2} -c -Wno-unused-result sound.c `sdl-config --cflags` exit #endif #include "SDL.h" #include <math.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include "sqlite3.h" #include "smallxrm.h" #include "quarks.h" #include "heromesh.h" #include "cursorshapes.h" typedef struct { Uint8*data; Uint32 len; // length in bytes } WaveSound; static Uint8 sound_on; static Sint16 mmlvolume=10000; static SDL_AudioSpec spec; static WaveSound*standardsounds; static Uint16 nstandardsounds; static WaveSound*usersounds; static Uint16 nusersounds; static Uint8**user_sound_names; static FILE*l_fp; static long l_offset,l_size; static float wavevolume=1.0; static Uint8 needs_amplify=0; static Uint32*mmltuning; static Uint32 mmltempo; static Uint8*volatile wavesound; static volatile Uint32 wavelen; static volatile Uint8 mmlsound[512]; static volatile Uint16 mmlpos; static volatile Uint32 mmltime; static void audio_callback(void*userdata,Uint8*stream,int len) { static Uint32 phase; if(wavesound) { if(wavelen<=len) { memcpy(stream,wavesound,wavelen); memset(stream+wavelen,0,len-wavelen); wavesound=0; wavelen=0; } else { memcpy(stream,wavesound,len); wavesound+=len; wavelen-=len; } } else if(mmlpos) { Uint16*out=(Uint16*)stream; Uint32 t=mmltime; int re=len>>1; int m=mmlpos; int n=mmlsound[m-1]; memset(stream,0,len); while(re) { if(n!=255) { phase+=mmltuning[n]; if(t>1) *out=phase&0x80000000U?-mmlvolume:mmlvolume; } if(!--t) { m+=2; if(m>=512) { m=0; break; } n=mmlsound[m-1]; t=mmltempo*mmlsound[m]; if(!t) { m=0; break; } } out++; re--; } mmlpos=m; mmltime=t; } else { memset(stream,0,len); } } static int my_seek(SDL_RWops*cxt,int o,int w) { switch(w) { case RW_SEEK_SET: fseek(l_fp,l_offset+o,SEEK_SET); break; case RW_SEEK_CUR: fseek(l_fp,o,SEEK_CUR); break; case RW_SEEK_END: fseek(l_fp,l_offset+l_size+o,SEEK_SET); break; } return ftell(l_fp)-l_offset; } static int my_read(SDL_RWops*cxt,void*ptr,int size,int maxnum) { if(size*maxnum+l_offset>ftell(l_fp)+l_size) maxnum=(ftell(l_fp)-l_offset)/size; return fread(ptr,size,maxnum,l_fp); } static int my_close(SDL_RWops*cxt) { return 0; } static SDL_RWops my_rwops={ .seek=my_seek, .read=my_read, .close=my_close, }; static void amplify_wave_sound(const WaveSound*ws) { Uint32 n; Sint16*b=(Sint16*)ws->data; Uint32 m=ws->len/sizeof(Sint16); if(!needs_amplify || !b) return; for(n=0;n<m;n++) b[n]*=wavevolume; } static void load_sound(FILE*fp,long offset,long size,WaveSound*ws) { SDL_AudioSpec src; SDL_AudioCVT cvt; Uint32 len=0; Uint8*buf=0; ws->data=0; ws->len=0; l_fp=fp; l_offset=offset; l_size=size; if(!SDL_LoadWAV_RW(&my_rwops,0,&src,&buf,&len)) { if(main_options['v']) fprintf(stderr,"[Cannot load wave audio at %ld (%ld bytes): %s]\n",offset,size,SDL_GetError()); return; } memset(&cvt,0,sizeof(SDL_AudioCVT)); if(SDL_BuildAudioCVT(&cvt,src.format,src.channels,src.freq,spec.format,spec.channels,spec.freq)<0) goto fail; cvt.buf=malloc(len*cvt.len_mult); cvt.len=len; if(!cvt.buf) goto fail; memcpy(cvt.buf,buf,len); if(SDL_ConvertAudio(&cvt)) goto fail; SDL_FreeWAV(buf); ws->data=cvt.buf; ws->len=cvt.len_cvt; amplify_wave_sound(ws); return; fail: if(main_options['v']) fprintf(stderr,"[Failed to convert wave audio at %ld (%ld bytes)]\n",offset,size); SDL_FreeWAV(buf); } static void load_sound_set(int is_user) { const char*v; char*nam; FILE*fp; Uint32 i,j; WaveSound*ws; if(is_user) { if(main_options['z']) { fp=composite_slice(".xclass",0); } else { nam=sqlite3_mprintf("%s.xclass",basefilename); if(!nam) return; fp=fopen(nam,"r"); sqlite3_free(nam); } if(!fp) return; } else { optionquery[2]=Q_standardSounds; v=xrm_get_resource(resourcedb,optionquery,optionquery,3); if(!v) return; fp=fopen(v,"r"); if(!fp) { fprintf(stderr,"Cannot open standard sounds file (%m)\n"); return; } nstandardsounds=N_STANDARD_SOUNDS+N_MESSAGES; standardsounds=malloc(nstandardsounds*sizeof(WaveSound)); if(!standardsounds) fatal("Allocation failed\n"); for(i=0;i<nstandardsounds;i++) standardsounds[i].data=0,standardsounds[i].len=0; } nam=malloc(256); for(;;) { for(i=0;;) { if(i==255) goto done; nam[i++]=j=fgetc(fp); if(j==(Uint32)EOF) goto done; if(!j) break; } i--; j=fgetc(fp)<<16; j|=fgetc(fp)<<24; j|=fgetc(fp)<<0; j|=fgetc(fp)<<8; l_offset=ftell(fp); l_size=j; if(i>4 && nam[i-4]=='.') { if(nam[i-3]>='a') nam[i-3]+='A'-'a'; if(nam[i-2]>='a') nam[i-2]+='A'-'a'; if(nam[i-1]>='a') nam[i-1]+='A'-'a'; } if(i>4 && nam[i-4]=='.' && nam[i-3]=='W' && nam[i-1]=='V' && (nam[i-2]=='A' || nam[i-2]=='Z')) { j=nam[i-2]; nam[i-4]=0; if(is_user) { if(nusersounds>255) goto done; i=nusersounds++; usersounds=realloc(usersounds,nusersounds*sizeof(WaveSound)); user_sound_names=realloc(user_sound_names,nusersounds*sizeof(Uint8*)); if(!usersounds || !user_sound_names) fatal("Allocation failed\n"); user_sound_names[i]=strdup(nam); if(!user_sound_names[i]) fatal("Allocation failed\n"); ws=usersounds+i; ws->data=0; ws->len=0; } else { for(i=0;i<N_STANDARD_SOUNDS;i++) if(!sqlite3_stricmp(nam,standard_sound_names[i])) goto found; for(i=0;i<N_MESSAGES;i++) if(!sqlite3_stricmp(nam,standard_message_names[i])) { i+=N_STANDARD_SOUNDS; goto found; } goto notfound; found: ws=standardsounds+i; if(ws->data) goto notfound; } if(j=='A') { load_sound(fp,l_offset,l_size,ws); } else { //TODO: Compressed sounds. } } notfound: fseek(fp,l_offset+l_size,SEEK_SET); } done: fclose(fp); free(nam); } void init_sound(void) { const char*v; double f; int i; optionquery[1]=Q_audio; optionquery[2]=Q_rate; v=xrm_get_resource(resourcedb,optionquery,optionquery,3); if(!v) return; spec.freq=strtol(v,0,10); optionquery[2]=Q_buffer; v=xrm_get_resource(resourcedb,optionquery,optionquery,3); if(!v) return; spec.samples=strtol(v,0,10); if(!spec.freq || !spec.samples) return; fprintf(stderr,"Initializing audio...\n"); spec.channels=1; spec.format=AUDIO_S16SYS; spec.callback=audio_callback; if(SDL_InitSubSystem(SDL_INIT_AUDIO)) { fprintf(stderr,"Cannot initalize audio subsystem.\n"); return; } if(SDL_OpenAudio(&spec,0)) { fprintf(stderr,"Cannot open audio device.\n"); return; } optionquery[2]=Q_waveVolume; if(v=xrm_get_resource(resourcedb,optionquery,optionquery,3)) { needs_amplify=1; wavevolume=strtod(v,0); } optionquery[2]=Q_mmlVolume; if(v=xrm_get_resource(resourcedb,optionquery,optionquery,3)) mmlvolume=fmin(strtod(v,0)*32767.0,32767.0); if(wavevolume>0.00001) { load_sound_set(0); // Standard sounds load_sound_set(1); // User sounds } if(mmlvolume) { mmltuning=malloc(256*sizeof(Uint32)); if(!mmltuning) fatal("Allocation failed\n"); optionquery[2]=Q_mmlTuning; if(v=xrm_get_resource(resourcedb,optionquery,optionquery,3)) f=strtod(v,0); else f=440.0; f*=0x80000000U/(double)spec.freq; for(i=0;i<190;i++) mmltuning[i]=f*pow(2.0,(i-96)/24.0); for(i=0;i<64;i++) mmltuning[i+190]=(((long long)(i+2))<<37)/spec.freq; optionquery[2]=Q_mmlTempo; if(v=xrm_get_resource(resourcedb,optionquery,optionquery,3)) i=strtol(v,0,10); else i=120; // Convert quarter notes per minute to samples per sixty-fourth note mmltempo=(spec.freq*60)/(i*16); } fprintf(stderr,"Done.\n"); wavesound=0; mmlpos=0; SDL_PauseAudio(0); sound_on=1; } static void set_mml(const unsigned char*s) { const Sint8 y[8]={2,0,4,-18,-14,-10,-8,-4}; int m=0; int o=96; int t=2; int n; while(*s && m<512) { switch(*s++) { case '0' ... '8': o=24*(s[-1]-'0'); break; case '-': if(o) o-=24; break; case '+': o+=24; break; case '.': t+=t/2; break; case 'A' ... 'G': case 'a' ... 'g': n=o+y[s[-1]&7]; while(*s) switch(*s) { case '!': n-=2; s++; break; case '#': n+=2; s++; break; case ',': n-=1; s++; break; case '\'': n+=1; s++; break; default: goto send; } goto send; case 'H': case 'h': t=32; break; case 'I': case 'i': t=8; break; case 'L': case 'l': t=0; while(*s>='0' && *s<='9') t=10*t+*s++-'0'; break; case 'N': case 'n': n=0; while(*s>='0' && *s<='9') n=10*n+*s++-'0'; send: mmlsound[m++]=n; mmlsound[m++]=t; break; case 'Q': case 'q': t=16; break; case 'S': case 's': t=4; break; case 'T': case 't': t=2; break; case 'W': case 'w': t=64; break; case 'X': case 'x': mmlsound[m++]=255; mmlsound[m++]=t; break; case 'Z': case 'z': t=1; break; } } if(!m) return; if(m<511) mmlsound[m+1]=0; mmlpos=1; mmltime=mmlsound[1]*mmltempo; } void set_sound_effect(Value v1,Value v2) { static const unsigned char*const builtin[8]={ "s.g", "scdefgab+c-bagfedc-c", "i1c+c+c+c+c+c+cx", "-cc'c#d,dd'd#ee'ff'f#", "sn190n191n192n193n194n195n196n197n198n199n200n201n202n203n204n205n206n207n208n209n210", "z+c-gec-gec", "t+c-gec", "tc-c+d-d+e-e+f-f+g-g", }; const unsigned char*s; if(!sound_on) return; if(!v2.t && !v2.u && (mmlpos || wavesound)) return; SDL_LockAudio(); wavesound=0; mmlpos=0; switch(v1.t) { case TY_MESSAGE: v1.u+=N_STANDARD_SOUNDS; //fallthrough case TY_SOUND: if(v1.u<nstandardsounds) { wavesound=standardsounds[v1.u].data; wavelen=standardsounds[v1.u].len; } break; case TY_USOUND: if(v1.u<nusersounds) { wavesound=usersounds[v1.u].data; wavelen=usersounds[v1.u].len; } break; case TY_STRING: case TY_LEVELSTRING: if(!mmlvolume) break; if(s=value_string_ptr(v1)) set_mml(s); break; case TY_FOR: // (only used for the sound test) if(!mmlvolume) break; set_mml(builtin[v1.u&7]); break; } SDL_UnlockAudio(); } Uint16 find_user_sound(const char*name) { int i; for(i=0;i<nusersounds;i++) if(!strcmp(name,user_sound_names[i])) return i+0x0400; return 0x03FF; } void set_sound_on(int on) { if(!sound_on) return; set_sound_effect(NVALUE(0),NVALUE(1)); SDL_PauseAudio(!on); } void sound_test(void) { Uint8 columns; SDL_Event ev; SDL_Rect r; int scroll=0; int nitems; int scrmax; int i,j,k,x,y; Value v; char buf[256]; if(main_options['T'] && main_options['v']) { if(mmltuning) printf("mmltempo=%d; mmlvolume=%d; mmltuning[96]=%d\n",(int)mmltempo,(int)mmlvolume,(int)mmltuning[96]); for(i=0;i<nusersounds;i++) printf("%d: %s (ptr=%p, len=%d bytes)\n",i,user_sound_names[i],usersounds[i].data,usersounds[i].len); fflush(stdout); } if(!screen || !sound_on) return; nitems=nstandardsounds+nusersounds+8; columns=(screen->w-16)/240?:1; scrmax=(nitems+columns-1)/columns; set_cursor(XC_arrow); redraw: SDL_FillRect(screen,0,0x02); r.x=r.y=0; r.w=screen->w; r.h=8; SDL_FillRect(screen,&r,0xF7); SDL_LockSurface(screen); draw_text(0,0," <F1> Mute <F2> Stop <F3> Status <ESC> Cancel",0xF7,0xF0); if(SDL_GetAudioStatus()==SDL_AUDIO_PLAYING) draw_text(0,0,"\x0E",0xF7,0xF1); for(i=scroll*columns,x=0,y=8;i<nitems;i++) { if(y>screen->h-24) break; if(i<nstandardsounds) { k=standardsounds[i].data?0xF9:0xF8; snprintf(buf,29,"%s",i<N_STANDARD_SOUNDS?standard_sound_names[i]:standard_message_names[i-N_STANDARD_SOUNDS]); //TODO } else if(i<nstandardsounds+nusersounds) { k=0xFA; snprintf(buf,29,"%s",user_sound_names[i-nstandardsounds]); } else { k=0xFB; snprintf(buf,29,"TEST %d",i-nstandardsounds-nusersounds); } r.x=x*240+20; r.y=y+4; r.w=232; r.h=16; SDL_FillRect(screen,&r,k); draw_text(r.x+4,r.y+4,buf,k,0xF0); if(++x==columns) x=0,y+=24; } SDL_UnlockSurface(screen); r.x=r.w=0; r.y=8; r.h=screen->h-8; scrollbar(&scroll,screen->h/24-1,scrmax,0,&r); SDL_Flip(screen); while(SDL_WaitEvent(&ev)) { if(ev.type!=SDL_VIDEOEXPOSE && scrollbar(&scroll,screen->h/24-1,scrmax,&ev,&r)) goto redraw; switch(ev.type) { case SDL_MOUSEMOTION: x=ev.button.x-16; y=ev.button.y-8; if(x<0 || y<0) goto arrow; i=x/240+columns*(y/24); x%=240; y%=24; if(x<4 || x>236 || y<4 || y>20 || i<0 || i>=nitems) goto arrow; set_cursor(XC_hand1); break; arrow: set_cursor(XC_arrow); break; case SDL_MOUSEBUTTONDOWN: x=ev.button.x-16; y=ev.button.y-8; if(x<0 || y<0) break; i=x/240+columns*(y/24)+scroll*columns; x%=240; y%=24; if(x<4 || x>236 || y<4 || y>20 || i<0 || i>=nitems) break; if(i<nstandardsounds) { if(i<N_STANDARD_SOUNDS) { v.t=TY_SOUND; v.u=i; } else { v.t=TY_MESSAGE; v.u=i-N_STANDARD_SOUNDS; } } else if(i<nstandardsounds+nusersounds) { v.t=TY_USOUND; v.u=i-nstandardsounds; } else { v.t=TY_FOR; v.u=i-nstandardsounds-nusersounds; } set_sound_effect(v,NVALUE(ev.button.button-1)); break; case SDL_KEYDOWN: switch(ev.key.keysym.sym) { case SDLK_ESCAPE: return; case SDLK_F1: set_sound_on(SDL_GetAudioStatus()==SDL_AUDIO_PAUSED); goto redraw; case SDLK_F2: set_sound_effect(NVALUE(0),NVALUE(1)); break; case SDLK_F3: snprintf(buf,255,"Sample rate: %d Hz\nBuffer size: %d samples (%d bytes)\nStatus: %d\nWave queue: %d bytes\nMML position: %d" ,(int)spec.freq,(int)spec.samples,(int)spec.size,(int)SDL_GetAudioStatus(),(int)wavelen,(int)mmlpos); modal_draw_popup(buf); goto redraw; case SDLK_HOME: case SDLK_KP7: scroll=0; goto redraw; case SDLK_UP: case SDLK_KP8: if(scroll) --scroll; goto redraw; case SDLK_DOWN: case SDLK_KP2: ++scroll; goto redraw; } break; case SDL_VIDEOEXPOSE: goto redraw; case SDL_QUIT: SDL_PushEvent(&ev); return; } } }