function MKS_05b % MKS 05b - TIME-VARYING AUDIO PROCESSOR / SOUND EFFECT DESIGN % Editable control tracks: F0(t), ZERO z0(t), POLE p(t), AMPLITUDE A(t). % Click: add/move point; drag: edit; right click near point: remove. % Shared timeline with input/output waveform; cursor shows instantaneous H(f). % Standalone MATLAB (no Audio Toolbox). Offline processing. fs=44100; C.bg=[.96 .97 .98]; C.blue=[.12 .39 .78]; C.red=[.85 .34 .19]; C.green=[.18 .58 .40]; C.gray=[.52 .56 .61]; S.fs=fs; S.x=[]; S.y=[]; S.file=''; S.player=[]; S.source=2; S.duration=5; S.f0=160; S.pw=.5; S.tEnd=5; S.cursor=0; S.dirty=true; S.drag=0; S.dragIndex=0; S.points={ [0 160;5 160], [0 0;5 0], [0 0;5 0], [0 1;5 1] }; S.names={'F0 (Hz)','ZERO z_0(t)','POLE p(t)','AMPLITUDE A(t)'}; S.limits=[20 2000;-1 1;-.98 .98;0 1]; S.colors={[.55 .27 .75],C.blue,C.red,C.green}; fig=figure('Name','MKS 05b - TIME-VARYING AUDIO PROCESSOR', ... 'NumberTitle','off','MenuBar','none','ToolBar','none','Color',C.bg, ... 'Position',[75 50 1390 845],'Resize','off', ... 'WindowButtonMotionFcn',@mouseMove,'WindowButtonUpFcn',@mouseUp, ... 'CloseRequestFcn',@closeApp); set(fig,'DefaultUicontrolFontName','Arial'); uicontrol(fig,'Style','text','String','MKS 05b - TIME-VARYING AUDIO PROCESSOR', ... 'Position',[26 806 1310 30],'BackgroundColor',C.bg, ... 'ForegroundColor',[.12 .18 .27],'FontSize',19,'FontWeight','bold', ... 'HorizontalAlignment','left'); % Six vertically aligned tracks, same horizontal time scale. left=70; wide=815; height=88; ys=[690 568 446 324 202 80]; S.ax=gobjects(1,6); S.lines=gobjects(1,6); S.nodes=gobjects(1,4); trackTitles={'INPUT / SOURCE','F0 (Hz)','ZERO z_0(t)', ... 'POLE p(t)','AMPLITUDE A(t)','FILTERED OUTPUT'}; for k=1:6 S.ax(k)=axes('Parent',fig,'Units','pixels','Position',[left ys(k) wide height]); hold(S.ax(k),'on'); grid(S.ax(k),'on'); title(S.ax(k),trackTitles{k},'FontWeight','bold','FontSize',10); xlim(S.ax(k),[0 S.tEnd]); if k==2,ylim(S.ax(k),[20 2000]);set(S.ax(k),'YScale','log'); elseif k==5,ylim(S.ax(k),[-.05 1.05]); else,ylim(S.ax(k),[-1.05 1.05]);end if k==6,xlabel(S.ax(k),'Time (s)');end end for k=1:4 a=S.ax(k+1); col=S.colors{k}; S.lines(k+1)=plot(a,nan,nan,'-','Color',col,'LineWidth',2, ... 'HitTest','off'); S.nodes(k)=plot(a,nan,nan,'o','Color',col,'MarkerFaceColor',col, ... 'MarkerSize',6,'HitTest','off'); set(a,'ButtonDownFcn',{@trackClick,k}); end S.lines(1)=plot(S.ax(1),nan,nan,'Color',C.blue,'HitTest','off'); S.lines(6)=plot(S.ax(6),nan,nan,'Color',C.red,'HitTest','off'); S.cursorLines=gobjects(1,6); for k=1:6 if k==2,cy=[20 2000];else,cy=[-1.1 1.1];end S.cursorLines(k)=plot(S.ax(k),[0 0],cy,':', ... 'Color',C.gray,'LineWidth',1,'HitTest','off'); end % Clicking waveforms moves time cursor. set(S.ax(1),'ButtonDownFcn',@cursorClick); set(S.ax(6),'ButtonDownFcn',@cursorClick); S.axH=axes('Parent',fig,'Units','pixels','Position',[1010 578 315 172]); title(S.axH,'FILTER RESPONSE','FontSize',11);grid(S.axH,'on'); xlabel(S.axH,'Frequency (Hz)');ylabel(S.axH,'dB'); S.axZ=axes('Parent',fig,'Units','pixels','Position',[1050 345 235 185]); title(S.axZ,'POLE / ZERO','FontSize',11);grid(S.axZ,'on'); uicontrol(fig,'Style','text','String','SOURCE', ... 'Position',[940 291 120 22],'BackgroundColor',C.bg,'FontWeight','bold', ... 'HorizontalAlignment','left'); S.ui.source=uicontrol(fig,'Style','popupmenu', ... 'String',{'WAV FILE','NOISE','SINE','TRIANGLE','SAW','PULSE'}, ... 'Value',2, 'Position',[1060 287 240 29],'FontSize',11,'Callback',@sourceChanged); uicontrol(fig,'Style','text','String','DURATION (s)', ... 'Position',[940 252 120 22],'BackgroundColor',C.bg,'HorizontalAlignment','left'); S.ui.duration=uicontrol(fig,'Style','edit','String','5', ... 'Position',[1060 249 95 29],'FontSize',11,'Callback',@sourceSettings); uicontrol(fig,'Style','text','String','F0 (Hz)', ... 'Position',[1170 252 90 22],'BackgroundColor',C.bg); S.ui.f0=uicontrol(fig,'Style','edit','String','160', ... 'Position',[1260 249 70 29],'FontSize',11,'Callback',@sourceSettings); uicontrol(fig,'Style','text','String','PULSE WIDTH (%)', ... 'Position',[940 211 155 22],'BackgroundColor',C.bg,'HorizontalAlignment','left'); S.ui.pw=uicontrol(fig,'Style','slider','Min',5,'Max',95,'Value',50, ... 'Position',[1100 210 165 24],'Callback',@sourceSettings); S.ui.pwLabel=uicontrol(fig,'Style','text','String','50', ... 'Position',[1270 211 60 22],'BackgroundColor',C.bg); % Transport grouped by workflow: left = input, right = output. S.ui.load=mkbutton(944,163,185,'LOAD WAV',@loadWav,[.24 .38 .55]); S.ui.generate=mkbutton(944,117,185,'GENERATE',@generateSource,C.blue); S.ui.playIn=mkbutton(944,71,185,'PLAY INPUT',@playInput,[.25 .52 .39]); S.ui.stop=mkbutton(944,25,185,'STOP',@stopPlayback,[.55 .34 .34]); S.ui.reset=mkbutton(1142,163,185,'RESET TRACKS',@resetTracks,[.48 .49 .52]); S.ui.process=mkbutton(1142,117,185,'PROCESS',@processAudio,C.blue); S.ui.playOut=mkbutton(1142,71,185,'PLAY OUTPUT',@playOutput,[.25 .52 .39]); S.ui.save=mkbutton(1142,25,185,'SAVE WAV',@saveOutput,[.34 .39 .50]); S.ui.status=uicontrol(fig,'Style','text','String', ... 'Click to add points; drag to move; right-click to delete. Click waveform for filter snapshot.', ... 'Position',[70 13 812 30],'BackgroundColor',C.bg, ... 'HorizontalAlignment','left','FontSize',10); S.ui.filename=uicontrol(fig,'Style','text','String','No WAV loaded', ... 'Position',[940 323 420 20],'BackgroundColor',C.bg, ... 'HorizontalAlignment','left','ForegroundColor',C.gray); % Start with generated noise: no external WAV needed. guidata(fig,S); updateSourceControls(); generateSource(); function color=ternaryColor(periodic) if periodic,color=[1 1 1];else,color=[.94 .94 .94];end end function x=synthesizeSource(q,N) % F0 track drives the oscillator's continuous phase (cycles). % Integral of instantaneous frequency prevents phase resets at nodes. if q.source==2 x=.25*randn(N,1);return; end if q.source==1 x=q.x;return; end tt=(0:N-1)'/q.fs; freq=interp1(q.points{1}(:,1),q.points{1}(:,2),tt,'linear','extrap'); freq=max(20,min(2000,freq)); phase=cumsum([0;freq(1:end-1)])/q.fs; ph=mod(phase,1); switch q.source case 3,x=.55*sin(2*pi*phase); case 4,x=.55*(4*abs(ph-.5)-1); case 5,x=.55*(2*ph-1); case 6,x=.55*(2*(ph=3; if periodic,onoff='on';else,onoff='off';end set(q.ui.f0,'Enable',onoff); set(q.ax(2),'Color',ternaryColor(periodic)); if q.source==6,po='on';else,po='off';end set(q.ui.pw,'Enable',po); end function sourceSettings(~,~) q=guidata(fig); q.duration=clamp(readNumber(q.ui.duration,q.duration),.1,60); newF0=clamp(readNumber(q.ui.f0,q.f0),20,2000); if newF0~=q.f0 q.points{1}(:,2)=newF0; q.dirty=true; end q.f0=newF0; q.pw=get(q.ui.pw,'Value')/100; set(q.ui.duration,'String',sprintf('%.2f',q.duration)); set(q.ui.f0,'String',sprintf('%.1f',q.f0)); set(q.ui.pwLabel,'String',sprintf('%.0f',100*q.pw)); guidata(fig,q); setStatus('Source settings changed. Press GENERATE.'); end function n=readNumber(h,default) n=str2double(strrep(get(h,'String'),',','.')); if ~isfinite(n),n=default;end end function generateSource(varargin) sourceSettingsNoStatus();q=guidata(fig); if q.source==1 setStatus('Select a WAV file using LOAD WAV.');return; end stopAudio(); N=max(1,round(q.duration*q.fs)); q=resizeTimeline(q,N/q.fs); x=synthesizeSource(q,N); q.x=x;q.y=[];q.file='';q.dirty=true; guidata(fig,q);refreshAll(); set(q.ui.filename,'String',sprintf('Generated: %d Hz, %.2f s',q.fs,q.tEnd)); setStatus('Source ready. Edit F0 / filter / AM tracks, then PROCESS.'); end function sourceSettingsNoStatus() q=guidata(fig); q.duration=clamp(readNumber(q.ui.duration,q.duration),.1,60); newF0=clamp(readNumber(q.ui.f0,q.f0),20,2000); if newF0~=q.f0 q.points{1}(:,2)=newF0; q.dirty=true; end q.f0=newF0; q.pw=get(q.ui.pw,'Value')/100; set(q.ui.duration,'String',sprintf('%.2f',q.duration)); set(q.ui.f0,'String',sprintf('%.1f',q.f0)); set(q.ui.pwLabel,'String',sprintf('%.0f',q.pw*100)); guidata(fig,q); end function loadWav(~,~) [name,folder]=uigetfile({'*.wav','WAV files (*.wav)'},'Select WAV'); if isequal(name,0),return;end try [x,sampleRate]=audioread(fullfile(folder,name)); if isempty(x),error('Empty audio file.');end if size(x,2)>2,x=x(:,1:2);end stopAudio();q=guidata(fig); q.x=x;q.y=[];q.fs=sampleRate;q.source=1; q.file=fullfile(folder,name);q.dirty=true; q=resizeTimeline(q,size(x,1)/sampleRate); set(q.ui.source,'Value',1); set(q.ui.filename,'String',sprintf('%s | %.2f s | %d Hz', ... name,q.tEnd,q.fs)); guidata(fig,q);updateSourceControls();refreshAll(); setStatus('WAV loaded. Edit control tracks and press PROCESS.'); catch ME errordlg(ME.message,'WAV error'); end end function q=resizeTimeline(q,newEnd) oldEnd=q.tEnd;q.tEnd=newEnd;q.cursor=min(q.cursor,newEnd); for j=1:4 pts=q.points{j}; if oldEnd>0,pts(:,1)=pts(:,1)*(newEnd/oldEnd);end pts(1,1)=0;pts(end,1)=newEnd; q.points{j}=pts; end end function resetTracks(~,~) q=guidata(fig); q.points={ [0 q.f0;q.tEnd q.f0],[0 0;q.tEnd 0], ... [0 0;q.tEnd 0],[0 1;q.tEnd 1] }; q.dirty=true;guidata(fig,q);refreshAll(); setStatus('Tracks reset: F0 constant, z0=0, p=0, A=1.'); end function trackClick(~,~,j) q=guidata(fig);a=q.ax(j+1);cp=get(a,'CurrentPoint'); tx=clamp(cp(1,1),0,q.tEnd); val=clamp(cp(1,2),q.limits(j,1),q.limits(j,2)); pts=q.points{j}; % Select existing node if near in normalized screen coordinates. span=q.limits(j,2)-q.limits(j,1); if j==1 yDist=(log(pts(:,2))-log(val))/(log(2000)-log(20)); else yDist=(pts(:,2)-val)/span; end dist=hypot((pts(:,1)-tx)/max(q.tEnd,eps),yDist); [d,idx]=min(dist); rightClick=strcmp(get(fig,'SelectionType'),'alt'); if rightClick if d<.045 && idx>1 && idx=.045 pts=[pts;tx val];pts=sortrows(pts,1); [~,idx]=min(abs(pts(:,1)-tx)); else pts(idx,2)=val; end if idx==1,pts(idx,1)=0;end if idx==size(pts,1),pts(idx,1)=q.tEnd;end q.points{j}=pts;q.drag=j;q.dragIndex=idx;q.dirty=true; q.cursor=tx;guidata(fig,q);refreshAll(); end function mouseMove(~,~) q=guidata(fig); if q.drag==0,return;end j=q.drag;idx=q.dragIndex;pts=q.points{j}; cp=get(q.ax(j+1),'CurrentPoint'); tx=clamp(cp(1,1),0,q.tEnd); val=clamp(cp(1,2),q.limits(j,1),q.limits(j,2)); if idx==1,tx=0; elseif idx==size(pts,1),tx=q.tEnd; else gap=max(q.tEnd*1e-5,1e-6); tx=clamp(tx,pts(idx-1,1)+gap,pts(idx+1,1)-gap); end pts(idx,:)=[tx val];q.points{j}=pts;q.cursor=tx;q.dirty=true; guidata(fig,q);refreshControl(j);refreshCursor();updateResponse(); end function mouseUp(~,~) q=guidata(fig);q.drag=0;guidata(fig,q); end function cursorClick(src,~) q=guidata(fig);cp=get(src,'CurrentPoint'); q.cursor=clamp(cp(1,1),0,q.tEnd); guidata(fig,q);refreshCursor();updateResponse(); end function refreshAll() q=guidata(fig); for j=1:6 xlim(q.ax(j),[0 max(q.tEnd,.001)]); end refreshWaveforms(); for j=1:4,refreshControl(j);end refreshCursor();updateResponse(); end function refreshWaveforms() q=guidata(fig); plotWave(q.lines(1),q.x,q.fs); plotWave(q.lines(6),q.y,q.fs); end function plotWave(h,x,sampleRate) if isempty(x),set(h,'XData',nan,'YData',nan);return;end N=size(x,1);step=max(1,ceil(N/4000));idx=1:step:N; set(h,'XData',(idx-1)/sampleRate,'YData',x(idx,1)); end function refreshControl(j) q=guidata(fig);pts=q.points{j}; set(q.lines(j+1),'XData',pts(:,1),'YData',pts(:,2)); set(q.nodes(j),'XData',pts(:,1),'YData',pts(:,2)); end function refreshCursor() q=guidata(fig); for j=1:6 set(q.cursorLines(j),'XData',[q.cursor q.cursor]); end end function updateResponse() q=guidata(fig);t=q.cursor; f0=interp1(q.points{1}(:,1),q.points{1}(:,2),t,'linear'); z=interp1(q.points{2}(:,1),q.points{2}(:,2),t,'linear'); p=interp1(q.points{3}(:,1),q.points{3}(:,2),t,'linear'); A=interp1(q.points{4}(:,1),q.points{4}(:,2),t,'linear'); w=linspace(0,pi,650);fr=w*q.fs/(2*pi); H=(1-z*exp(-1i*w))./(1-p*exp(-1i*w)); plot(q.axH,fr,20*log10(max(abs(H),1e-6)),'Color',C.blue,'LineWidth',1.6); xlim(q.axH,[0 q.fs/2]);grid(q.axH,'on'); title(q.axH,sprintf('FILTER RESPONSE | t=%.2fs',t),'FontSize',10); xlabel(q.axH,'Frequency (Hz)');ylabel(q.axH,'dB'); th=linspace(0,2*pi,250); cla(q.axZ);hold(q.axZ,'on'); plot(q.axZ,cos(th),sin(th),'Color',C.gray); plot(q.axZ,[-1.1 1.1],[0 0],':','Color',C.gray); plot(q.axZ,[0 0],[-1.1 1.1],':','Color',C.gray); plot(q.axZ,z,0,'o','Color',C.blue,'MarkerSize',10,'LineWidth',2); plot(q.axZ,p,0,'x','Color',C.red,'MarkerSize',11,'LineWidth',2); hold(q.axZ,'off');axis(q.axZ,'equal'); xlim(q.axZ,[-1.1 1.1]);ylim(q.axZ,[-1.1 1.1]);grid(q.axZ,'on'); title(q.axZ,sprintf('F0=%.0f Hz | z=%.2f p=%.2f A=%.2f',f0,z,p,A),'FontSize',9); end function processAudio(~,~) q=guidata(fig); if isempty(q.x),setStatus('Load WAV or GENERATE a source first.');return;end stopAudio();setStatus('Processing...');drawnow; try N=size(q.x,1);nch=size(q.x,2); tt=(0:N-1)'/q.fs; if q.source>=3 q.x=synthesizeSource(q,N); guidata(fig,q); end z=interp1(q.points{2}(:,1),q.points{2}(:,2),tt,'linear'); p=interp1(q.points{3}(:,1),q.points{3}(:,2),tt,'linear'); amp=interp1(q.points{4}(:,1),q.points{4}(:,2),tt,'linear'); % Preserve filter states independently for each channel. y=zeros(N,nch);xp=zeros(1,nch);yp=zeros(1,nch); for n=1:N v=q.x(n,:)-z(n)*xp+p(n)*yp; y(n,:)=amp(n)*v; xp=q.x(n,:);yp=v; end peak=max(abs(y(:))); scale=min(1,.98/max(peak,eps));y=y*scale; q.y=y;q.dirty=false;guidata(fig,q); refreshWaveforms(); setStatus(sprintf('Processed %.2f s | pre-limiter peak %.2f | gain %.3f', ... q.tEnd,peak,scale)); catch ME errordlg(ME.message,'Processing error');setStatus('Processing failed.'); end end function playInput(~,~) q=guidata(fig); if isempty(q.x),setStatus('Generate or load a source first.');return;end playSignal(q.x,q.fs,'Playing input...'); end function playOutput(~,~) q=guidata(fig); if isempty(q.y)||q.dirty setStatus('Press PROCESS after editing the tracks.');return; end playSignal(q.y,q.fs,'Playing output...'); end function playSignal(x,sampleRate,msg) stopAudio(); try pl=audioplayer(x,sampleRate); q=guidata(fig);q.player=pl;guidata(fig,q); play(pl);setStatus(msg); catch ME,errordlg(ME.message,'Playback error');end end function stopPlayback(~,~) stopAudio();setStatus('Stopped.'); end function stopAudio() if ~ishandle(fig),return;end q=guidata(fig); if ~isempty(q.player) try,stop(q.player);catch,end q.player=[];guidata(fig,q); end end function saveOutput(~,~) q=guidata(fig); if isempty(q.y)||q.dirty,setStatus('Press PROCESS before SAVE WAV.');return;end [name,folder]=uiputfile('*.wav','Save processed WAV','MKS_05b_output.wav'); if isequal(name,0),return;end try audiowrite(fullfile(folder,name),q.y,q.fs,'BitsPerSample',24); setStatus(['Saved: ' fullfile(folder,name)]); catch ME,errordlg(ME.message,'Save error');end end function setStatus(msg) if ~ishandle(fig),return;end q=guidata(fig);set(q.ui.status,'String',msg); end function closeApp(~,~) stopAudio();if ishandle(fig),delete(fig);end end end function x=clamp(x,a,b) x=max(a,min(b,x)); end