diff --git a/DESCRIPTION b/DESCRIPTION index b6fbf3c..92554b5 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,8 +1,8 @@ Package: dynaSpec Type: Package Title: Dynamic Spectrogram Visualizations -Version: 1.0.1 -Date: 2021-03-09 +Version: 1.0.2 +Date: 2024-02-20 Description: A set of tools to generate dynamic spectrogram visualizations in video format. License: GPL (>= 2) Imports: utils, grDevices, graphics, seewave, tuneR, grid, png, ggplot2, viridis, scales, ari, gganimate, warbleR diff --git a/R/ggSpec.R b/R/ggSpec.R index 5ddeec7..524c61b 100644 --- a/R/ggSpec.R +++ b/R/ggSpec.R @@ -1,109 +1,212 @@ # ggSpec: helper function to create ggplot spectrogram of wave file -ggSpec<-function(wav,soundFile,segLens,savePNG,specWidth,specHeight,destFolder,ovlp,wl,wn,yLim,xLim,colPal,colbins,ampTrans,plotLegend,onlyPlotSpec,isViridis,bg,fontAndAxisCol,min_dB,bgFlood,optim,timeAdj,...) -{ - if(missing(segLens)){nSegs=1}else{ - nSegs=length(segLens)-1 - } - - - # bg="#ebe834" - #Font color adapted from https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color - - if(is.null(fontAndAxisCol)){ - autoFontCol=TRUE - bgRGB<-grDevices::col2rgb(bg) - fontAndAxisCol<-if (bgRGB["red",1]*0.299 + bgRGB["green",1]*0.587 + bgRGB["blue",1]*0.114 > 149){"#000000"}else{"#ffffff"} - }else{autoFontCol=FALSE} - #For testing font contrast against bg - # par(bg=bg) - # plot(1,1,col="transparent") - # text(1,1,"READABLE?",cex=5,col=contrastFont) - - - #Modified from seewave::ggspectro - #---> - # norm =FALSE is important for having similar gain across different recordings - # spectrogram<-seewave::spectro(wav,plot=FALSE,ovlp=ovlp,wl=wl,wn=wn,norm=FALSE) - #normalize(wav,center=FALSE,pcm=TRUE) - spectrogram<-seewave::spectro(wav,plot=FALSE,ovlp=ovlp,wl=wl,wn=wn,...) - freq <- rep(spectrogram$freq, times = ncol(spectrogram$amp)) - time <- rep(spectrogram$time, each = nrow(spectrogram$amp)) - amplitude <- as.vector(spectrogram$amp) - #<-------- - df <- data.frame(time,freq,amplitude) - - - #experimental code to simplify the tibble for generating spec data - if(!is.null(optim)){ - #nothing here yet - } - - ##### - #Plot spec for every segment, saving specs as list for output - Glist<-list() - for (i in 1:(nSegs)){ +ggSpec <- + function(wav, + soundFile, + segLens, + savePNG, + specWidth, + specHeight, + destFolder, + ovlp, + wl, + wn, + yLim, + xLim, + colPal, + colbins, + ampTrans, + plotLegend, + onlyPlotSpec, + isViridis, + bg, + fontAndAxisCol, + min_dB, + bgFlood, + optim, + timeAdj, + ...) + { + if (missing(segLens)) { + nSegs = 1 + } else{ + nSegs = length(segLens) - 1 + } + + + # bg="#ebe834" + #Font color adapted from https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color + + if (is.null(fontAndAxisCol)) { + autoFontCol = TRUE + bgRGB <- grDevices::col2rgb(bg) + fontAndAxisCol <- + if (bgRGB["red", 1] * 0.299 + bgRGB["green", 1] * 0.587 + bgRGB["blue", 1] * + 0.114 > 149) { + "#000000" + } else{ + "#ffffff" + } + } else{ + autoFontCol = FALSE + } + #For testing font contrast against bg + # par(bg=bg) + # plot(1,1,col="transparent") + # text(1,1,"READABLE?",cex=5,col=contrastFont) - #subset df for segments - if(nSegs>1){ - if(i==1){ - cat("\nSpectrogram ggplots of segmented WAVs being generated\n") - } - if(i==nSegs){ - df_i <- subset(df,time>=segLens[i]&time<=segLens[i+1]) #last segment is inclusive on left & right bounds - }else{ - df_i<-subset(df,time>=segLens[i]&time + # norm =FALSE is important for having similar gain across different recordings + # spectrogram<-seewave::spectro(wav,plot=FALSE,ovlp=ovlp,wl=wl,wn=wn,norm=FALSE) + #normalize(wav,center=FALSE,pcm=TRUE) + spectrogram <- + seewave::spectro( + wav, + plot = FALSE, + ovlp = ovlp, + wl = wl, + wn = wn, + ... + ) + freq <- rep(spectrogram$freq, times = ncol(spectrogram$amp)) + time <- rep(spectrogram$time, each = nrow(spectrogram$amp)) + amplitude <- as.vector(spectrogram$amp) + #<-------- + df <- data.frame(time, freq, amplitude) + + + #experimental code to simplify the tibble for generating spec data + if (!is.null(optim)) { + #nothing here yet + } + + ##### + #Plot spec for every segment, saving specs as list for output + Glist <- list() + for (i in 1:(nSegs)) { + #subset df for segments + if (nSegs > 1) { + if (i == 1) { + cat("\nSpectrogram ggplots of segmented WAVs being generated\n") + } + if (i == nSegs) { + df_i <- + subset(df, time >= segLens[i] & + time <= segLens[i + 1]) #last segment is inclusive on left & right bounds + } else{ + df_i <- + subset(df, time >= segLens[i] & + time < segLens[i + 1]) #all other segs inclusive on left + } + + } else{ + df_i = df } - }else{df_i=df} - - ..level.. <- NULL - - #Plot that thang - Glist[[i]]<-ggplot2::ggplot(df_i,ggplot2::aes(x=time,y=freq,z=amplitude))+ggplot2::xlim(segLens[i],segLens[i+1])+ggplot2::ylim(yLim)+ - #Labels - ggplot2::labs(x="Time (s)",y="Frequency (kHz)",fill="Amplitude\n(dB)\n")+{ - #Set scale according to viridis or custom color scheme - if(isViridis){viridis::scale_fill_viridis(limits=c(min_dB,0),na.value="transparent",option=colPal,trans=scales::modulus_trans(p=ampTrans))}else{ - ggplot2::scale_fill_gradient(limits=c(min_dB,0),na.value="transparent",low=colPal[1],high=colPal[2],trans=scales::modulus_trans(p=ampTrans))} - }+ - #Make contours - ggplot2::stat_contour(geom="polygon",ggplot2::aes(fill=..level..),bins=colbins,na.rm=TRUE)+ - #Set base theme - mytheme(bg)+{ - #If user supplied fontAndAxisCol, change those settings (regardless of whether bg is flooded or not) - if(!autoFontCol){ - ggplot2::theme(axis.text=ggplot2::element_text(colour=fontAndAxisCol),text=ggplot2::element_text(colour=fontAndAxisCol),axis.line = ggplot2::element_line(colour=fontAndAxisCol),axis.ticks=ggplot2::element_line(colour=fontAndAxisCol)) - }else{} - }+{ - #get rid of axes & legend if requested - if(onlyPlotSpec){ggplot2::theme_void()+ ggplot2::theme(plot.background=ggplot2::element_rect(fill=bg),text=ggplot2::element_text(colour=fontAndAxisCol)) - }else{ - #For cases where axes are plotted + + level=NULL #just to shut up the check + + #Plot that thang + # # + Glist[[i]] <- + ggplot2::ggplot(df_i, ggplot2::aes(x = time, y = freq, z = amplitude)) + + ggplot2::xlim(segLens[i], segLens[i + 1]) + ggplot2::ylim(yLim) + + #Labels + ggplot2::labs(x = "Time (s)", y = "Frequency (kHz)", fill = "Amplitude\n(dB)\n") + + { + #Set scale according to viridis or custom color scheme + if (isViridis) { + viridis::scale_fill_viridis( + limits = c(min_dB, 0), + na.value = "transparent", + option = colPal, + trans = scales::modulus_trans(p = ampTrans) + ) + } else{ + ggplot2::scale_fill_gradient( + limits = c(min_dB, 0), + na.value = "transparent", + low = colPal[1], + high = colPal[2], + trans = scales::modulus_trans(p = ampTrans) + ) + } + } + + #Make contours + ggplot2::stat_contour( + geom = "polygon", + ggplot2::aes(fill = ggplot2::after_stat(level)), + bins = colbins, + na.rm = TRUE + ) + + #Set base theme + mytheme(bg) + { + #If user supplied fontAndAxisCol, change those settings (regardless of whether bg is flooded or not) + if (!autoFontCol) { + ggplot2::theme( + axis.text = ggplot2::element_text(colour = fontAndAxisCol), + text = ggplot2::element_text(colour = fontAndAxisCol), + axis.line = ggplot2::element_line(colour = fontAndAxisCol), + axis.ticks = ggplot2::element_line(colour = fontAndAxisCol) + ) + } else{ + } + } + { + #get rid of axes & legend if requested + if (onlyPlotSpec) { + ggplot2::theme_void() + ggplot2::theme( + plot.background = ggplot2::element_rect(fill = bg), + text = ggplot2::element_text(colour = fontAndAxisCol) + ) + } else{ + #For cases where axes are plotted # if axes to be plotted, flood panel bg color over axis area? - if(bgFlood){ggplot2::theme(plot.background=ggplot2::element_rect(fill=bg),axis.text=ggplot2::element_text(colour=fontAndAxisCol),text=ggplot2::element_text(colour=fontAndAxisCol),axis.line = ggplot2::element_line(colour=fontAndAxisCol),axis.ticks=ggplot2::element_line(colour=fontAndAxisCol),legend.background=ggplot2::element_rect(fill=bg))}else{} + if (bgFlood) { + ggplot2::theme( + plot.background = ggplot2::element_rect(fill = bg), + axis.text = ggplot2::element_text(colour = fontAndAxisCol), + text = ggplot2::element_text(colour = fontAndAxisCol), + axis.line = ggplot2::element_line(colour = fontAndAxisCol), + axis.ticks = ggplot2::element_line(colour = fontAndAxisCol), + legend.background = ggplot2::element_rect(fill = bg) + ) + } else{ } - }+{ - #Get rid of plotLegend if requested - if(!plotLegend){ggplot2::theme(legend.position = "none")}else{ggplot2::theme(legend.position = "right")} + } + } + { + #Get rid of plotLegend if requested + if (!plotLegend) { + ggplot2::theme(legend.position = "none") + } else{ + ggplot2::theme(legend.position = "right") + } }#end GGPLOT stuffs - - #Save plots if requested - if(savePNG){ - if(i==1){ - baseNom<-basename(tools::file_path_sans_ext(soundFile)) - subDest<-fs::path(destFolder,paste0(baseNom,"_static_specs")) - dir.create(subDest,showWarnings = FALSE) + + #Save plots if requested + if (savePNG) { + if (i == 1) { + baseNom <- basename(tools::file_path_sans_ext(soundFile)) + subDest <- + fs::path(destFolder, paste0(baseNom, "_static_specs")) + dir.create(subDest, showWarnings = FALSE) } - fn_i=fs::path(subDest,paste0(baseNom,"_",i),ext="png") - ggplot2::ggsave(fn_i,width=specWidth,height=specHeight,units="in") - cat(paste0("\nStatic spec saved @",fn_i)) - } + fn_i = fs::path(subDest, paste0(baseNom, "_", i), ext = "png") + ggplot2::ggsave(fn_i, + width = specWidth, + height = specHeight, + units = "in") + cat(paste0("\nStatic spec saved @", fn_i)) + } + + }#end for loop - }#end for loop - - rm(spectrogram) - -return(list(specList=Glist,fontAndAxisCol=fontAndAxisCol,autoFontCol=autoFontCol)) -}#end - + rm(spectrogram) + + return(list( + specList = Glist, + fontAndAxisCol = fontAndAxisCol, + autoFontCol = autoFontCol + )) + }#end diff --git a/R/paged_spectro.R b/R/paged_spectro.R index 8766124..2e4e059 100644 --- a/R/paged_spectro.R +++ b/R/paged_spectro.R @@ -124,6 +124,7 @@ for(i in 1:length(specParams$segWavs)) spec_height_px<-attributes(spec_PNG)$dim[1] #Create data frame for highlighting box animation for i^th wav segment + range_i<-c((i-1)*specParams$xLim[2],(i-1)*specParams$xLim[2]+specParams$xLim[2]) cursor<-seq(range_i[1],range_i[2],specParams$xLim[2]/framerate) played<-data.frame(xmin=cursor,xmax=rep(range_i[2],length(cursor)),ymin=rep(specParams$yLim[1],length(cursor)),ymax=rep(specParams$yLim[2], length(cursor))) diff --git a/R/prep_static_ggspectro.R b/R/prep_static_ggspectro.R index c2fea08..0791e74 100644 --- a/R/prep_static_ggspectro.R +++ b/R/prep_static_ggspectro.R @@ -18,7 +18,7 @@ #' @param savePNG save static spectrograms as PNGs? They will be exported to destFolder #' @param colPal color palette; one of "viridis","magma","plasma","inferno","cividis" from the \code{\link[viridis]{viridis}} package OR a 2 value vector (e.g. c("white","black")), defining the start and end of a custom color gradient #' @param crop subset of recording to include; if crop=NULL, use whole file; if number, interpreted as crop first X.X sec; if c(X1,X2), interpreted as specific time interval in sec -#' @param xLim is the time limit in seconds for all spectrograms; i.e. page width in seconds for multi-page dynamic spectrograms (defaults to WAV file length, unless file duration >5s) +#' @param xLim is the time limit in seconds for all spectrograms; i.e. page width in seconds for multi-page dynamic spectrograms (defaults to WAV file length, unless file duration >5s). To override the 5s limit, put xLim=Inf. #' @param yLim is the frequency limits (y-axis); default is c(0,10) aka 0-10kHz #' @param plotLegend include a legend showing amplitude colors? #' @param onlyPlotSpec do you want to just plot the spec and leave out the legend, axes, and axis labels? diff --git a/R/processSound.R b/R/processSound.R index bc9b1b9..3f21d17 100644 --- a/R/processSound.R +++ b/R/processSound.R @@ -1,70 +1,144 @@ # processSound: filter, crop, and segment wav (internal function) -processSound<-function(wav0,filter,ampThresh,crop,xLim,...){ - smplRt<-wav0@samp.rate - fileDur0<-max(length(wav0@left),length(wav0@right))/smplRt +processSound <- function(wav0, filter, ampThresh, crop, xLim, ...) { + smplRt <- wav0@samp.rate + fileDur0 <- max(length(wav0@left), length(wav0@right)) / smplRt ##Figure out crop & spectrogram temporal window width #If crop provided as single digit, interpret as first X sec - if(!is.null(crop)){ - if(length(crop)==1){if(crop==F){crop <- c(0,fileDur0)}else{ - crop <- c(0,crop)}} - #If supplied crop longer than wav, treated as max length & crop set to duration - if(crop[2]>fileDur0){ - crop <- c(0,fileDur0) - cat(paste0("\n**** Crop longer than file duration: ",round(fileDur0,2),", treated as max length & ignored"))} - }else{ - #If crop not supplied, default crop to duration, unless >10 sec, warning user - if(fileDur0>10){ - crop=c(0,10) - cat("\n*****\nCropping to 1st 10 sec due to slow processing of large WAV files.\nTo override, set crop=F or crop=c(startInSec,stopInSec)\n*****\n") - }else{crop=c(0,fileDur0)} + if (!is.null(crop)) { + if (length(crop) == 1) { + if (crop == F) { + crop <- c(0, fileDur0) + } else{ + crop <- c(0, crop) + } + } + #If supplied crop longer than wav, treated as max length & crop set to duration + if (crop[2] > fileDur0) { + crop <- c(0, fileDur0) + cat( + paste0( + "\n**** Crop longer than file duration: ", + round(fileDur0, 2), + ", treated as max length & ignored" + ) + ) + } + } else{ + #If crop not supplied, default crop to duration, unless >10 sec, warning user + if (fileDur0 > 10) { + crop = c(0, 10) + cat( + "\n*****\nCropping to 1st 10 sec due to slow processing of large WAV files.\nTo override, set crop=F or crop=c(startInSec,stopInSec)\n*****\n" + ) + } else{ + crop = c(0, fileDur0) + } } #crop is set now for all cases #assign new wave file for use going forward #the bit=wav0@bit is REALLY important! Should be standard, but it doesn't work - wav<-if(crop[2]==fileDur0){wav0}else{seewave::cutw(wav0,from=crop[1],to=crop[2],output="Wave",bit=wav0@bit)} - wavDur<- max(length(wav@left),length(wav@right))/wav@samp.rate + wav <- + if (crop[2] == fileDur0) { + wav0 + } else{ + seewave::cutw( + wav0, + from = crop[1], + to = crop[2], + output = "Wave", + bit = wav0@bit + ) + } + wavDur <- max(length(wav@left), length(wav@right)) / wav@samp.rate #Apply filters - if(!is.null(filter)){ - wav=seewave::ffilter(wave=wav,from=filter[1]*1000,to=filter[2]*1000,bandpass=F,output="Wave",rescale=T)} - if(ampThresh!=0){wav<-seewave::afilter(wav,f=smplRt,threshold=ampThresh,plot=F,output="Wave")} + if (!is.null(filter)) { + wav = seewave::ffilter( + wave = wav, + from = filter[1] * 1000, + to = filter[2] * 1000, + bandpass = F, + output = "Wave", + rescale = T + ) + } + if (ampThresh != 0) { + wav <- + seewave::afilter( + wav, + f = smplRt, + threshold = ampThresh, + plot = F, + output = "Wave" + ) + } ##Deal with xLim for segmenting wavs # if xLim not provided, default to smaller of 5sec or wav duration - if(is.null(xLim)){ - xLim<-c(0,min(5,wavDur)) - if(xLim[2]==5){ - cat("\n*****\nxLim set to 5 sec by default; define to override\n*****\n") + + if (is.null(xLim)) { + xLim <- c(0, min(5, wavDur)) + if (xLim[2] == 5) { + cat("\n*****\nxLim set to 5 sec by default; define to override\n*****\n") } - }else{ + } else{ #If xLim provided as single digit, interpret as X sec - if(length(xLim)==1){xLim <- c(0,xLim)} + if (length(xLim) == 1) { + xLim <- c(0, xLim) } + } #Add silence at the end if (user-supplied) xLim>cropped Duration or xLim doesn't divide into even segments of wave duration - timeRemainder<-(ceiling(wavDur/xLim[2])*xLim[2]-wavDur) > 0.001#(wavDur%/%xLim[2]-wavDur/xLim[2] - if(xLim[2]>wavDur|timeRemainder){ - if(timeRemainder){ - diffT<-ceiling(wavDur/xLim[2])*xLim[2]-wavDur - }else{ - diffT<-xLim[2]-wavDur - } - fillerWAV<-tuneR::silence(duration=diffT,samp.rate=smplRt,xunit="time",pcm=T,bit=wav@bit) - wav<-tuneR::bind(wav,fillerWAV) - #pastew results in intermittent problems! Don't use! bind seems much more dependable - #seewave::pastew(wave1=fillerWAV,wave2=wav,at="end",output="Wave",join=T,bit=wav0@bit) - wavDur<- max(length(wav@left),length(wav@right))/wav@samp.rate + timeRemainder <- + (ceiling(wavDur / xLim[2]) * xLim[2] - wavDur) > 0.001#(wavDur%/%xLim[2]-wavDur/xLim[2] + #If user wants to have xLim be Infinite (override 5sec max for long files) + if(xLim[2] %in% c(Inf,"Inf")){ + wavDur <- max(length(wav@left), length(wav@right)) / wav@samp.rate + xLim[2] <- wavDur + #Else fill out remainders with filler silence + }else if (xLim[2] > wavDur | timeRemainder ) { + if (timeRemainder) { + diffT <- ceiling(wavDur / xLim[2]) * xLim[2] - wavDur + } else{ + diffT <- xLim[2] - wavDur } + fillerWAV <- + tuneR::silence( + duration = diffT, + samp.rate = smplRt, + xunit = "time", + pcm = T, + bit = wav@bit + ) + wav <- tuneR::bind(wav, fillerWAV) + #pastew results in intermittent problems! Don't use! bind seems much more dependable + #seewave::pastew(wave1=fillerWAV,wave2=wav,at="end",output="Wave",join=T,bit=wav0@bit) + wavDur <- max(length(wav@left), length(wav@right)) / wav@samp.rate + } #Segment wav or make list of 1 if no segmentation - segLens <- seq(0,wavDur,xLim[2]) - indx<- 1:(length(segLens)-1) - segWavs<-lapply(indx,function(i) seewave::cutw(wav,from=segLens[i],to=segLens[i+1],output="Wave",bit=wav0@bit)) + segLens <- seq(0, wavDur, xLim[2]) + indx <- 1:(length(segLens) - 1) + segWavs <- + lapply(indx, function(i) + seewave::cutw( + wav, + from = segLens[i], + to = segLens[i + 1], + output = "Wave", + bit = wav0@bit + )) #browser() - return(list(newWav=wav,segWavs=segWavs,wavDur=wavDur,segLens=segLens,xLim=xLim)) + return(list( + newWav = wav, + segWavs = segWavs, + wavDur = wavDur, + segLens = segLens, + xLim = xLim + )) }#End processSound diff --git a/man/prep_static_ggspectro.Rd b/man/prep_static_ggspectro.Rd index 8c95758..af48465 100644 --- a/man/prep_static_ggspectro.Rd +++ b/man/prep_static_ggspectro.Rd @@ -28,7 +28,7 @@ colbins=30,ampThresh=0,bgFlood=FALSE,fontAndAxisCol=NULL,optim=NULL,...) \item{filter}{apply a bandpass filter? Defaults to none (NULL). Expects 'c(0,2)' where sound from 0 to 2kHz would be filtered out} -\item{xLim}{is the time limit in seconds for all spectrograms; i.e. page width in seconds for multi-page dynamic spectrograms (defaults to WAV file length, unless file duration >5s)} +\item{xLim}{is the time limit in seconds for all spectrograms; i.e. page width in seconds for multi-page dynamic spectrograms (defaults to WAV file length, unless file duration >5s). To override the 5s limit, put xLim=Inf.} \item{yLim}{is the frequency limits (y-axis); default is c(0,10) aka 0-10kHz}