Binary manipulation...

Hi guys and gals I am new here and a complete novice on/to AppleScript.
(Apologies for any typos.)

I have searched everywhere to manipulate binary, that is 0x00 to 0xFF, so I had to resort to going down a level to the bash shell and let that do it for me.

I am going to jump in at the deep end and try to do an Audio Function Generator.

To show I mean business I bragged about writing a sophisticiated shell script as a text mode calibrated AudioScope, from DC to around 20KHz, using the MBP inline 4 pole mic/ear socket.

You will find the full history of it here so far, it has had a magazine write up and has 5 stars here:-

http://www.unix.com/shell-programming-and-scripting/212939-start-simple-audio-scope-shell-script-12.html

I knew absolutely nothing about shell scripting in Jan 2013 when I started this project much like ApplScripting now.

So you guys and gals are going to be my mentors, hopefully I can create an Arbitratry Audio Function Generator using basic Apple Scripting. This will be developed on an MBP, circa August 2012, OSX 10.7.5 with full XCode and used on a virgin iMac OSX 10.11.5 default install.

Here is the very beginning of my attempt at Applescript-ing and works perfectly well.

My question is it is ugly and relies on shell commands to create the binary files, is there an official way in AppleScripts to create binary files, simply?

Just noticed the word-wrapping, be aware…

Is there a better way?

TIA.

Bazza, G0LCU.

do shell script "#!/bin/bash
> /tmp/sinewave.wav
> /tmp/squarewave.wav
printf '%b' '\\x52\\x49\\x46\\x46\\x24\\x00\\x01\\x00\\x57\\x41\\x56\\x45\\x66\\x6d\\x74\\x20\\x10\\x00\\x00\\x00\\x01\\x00\\x01\\x00\\x40\\x1f\\x00\\x00\\x40\\x1f\\x00\\x00\\x01\\x00\\x08\\x00\\x64\\x61\\x74\\x61\\x00\\x00\\x01\\x00' >> /tmp/sinewave.wav
printf '%b' '\\x52\\x49\\x46\\x46\\x24\\x00\\x01\\x00\\x57\\x41\\x56\\x45\\x66\\x6d\\x74\\x20\\x10\\x00\\x00\\x00\\x01\\x00\\x01\\x00\\x40\\x1f\\x00\\x00\\x40\\x1f\\x00\\x00\\x01\\x00\\x08\\x00\\x64\\x61\\x74\\x61\\x00\\x00\\x01\\x00' >> /tmp/squarewave.wav
for waveform in {0..8191}
do
	printf '%b' '\\x80\\x26\\x00\\x26\\x7F\\xD9\\xFF\\xD9' >> /tmp/sinewave.wav
	printf '%b' '\\xFF\\xFF\\xFF\\xFF\\x00\\x00\\x00\\x00' >> /tmp/squarewave.wav
done
"
repeat
	display dialog "Audio Function Generator, Version 0.00.01." buttons {"Sine", "Square", "Quit"} default button "Quit"
	if result = {button returned:"Sine"} then
		do shell script "/usr/bin/afplay /tmp/sinewave.wav"
	else if result = {button returned:"Square"} then
		do shell script "/usr/bin/afplay /tmp/squarewave.wav"
	else if result = {button returned:"Quit"} then
		exit repeat
	end if
end repeat

Hi Bazza. Welcome to MacScripter.

It’s fairly easy in AppleScript to write binary data to a file if you know what it is when you write the script and don’t need to manipulate it on the fly. There’s a limit on the lengths of data objects written into the source code, but not (as far as I know) in the sizes of data a script can handle.

set rootFolder to "/tmp/"
set headerData to «data rdat524946462400010057415645666D74201000000001000100401F0000401F0000010008006461746100000100»

set sinewaveData to «data rdat802600267FD9FFD9»
set sinewavePath to rootFolder & "sinewave.wav"
writeWAVToFile(POSIX file sinewavePath, headerData, sinewaveData, 8192)

set squarewaveData to «data rdatFFFFFFFF00000000»
set squarewavePath to rootFolder & "squarewave.wav"
writeWAVToFile(POSIX file squarewavePath, headerData, squarewaveData, 8192)

repeat
	set theChoice to button returned of (display dialog "Audio Function Generator, Version 0.00.01." buttons {"Sine", "Square", "Quit"} default button "Quit")
	if (theChoice is "Sine") then
		do shell script "/usr/bin/afplay " & quoted form of sinewavePath
	else if (theChoice is "Square") then
		do shell script "/usr/bin/afplay " & quoted form of squarewavePath
	else
		exit repeat
	end if
end repeat

on writeWAVToFile(ASFileOrAlias, headerData, waveData, numberOfCycles)
	set fRef to (open for access ASFileOrAlias with write permission)
	try
		set eof fRef to 0
		write headerData to fRef
		repeat numberOfCycles times
			write waveData to fRef starting at eof
		end repeat
		close access fRef
	on error errMsg
		close access fRef
		display dialog errMsg buttons {"OK"} default button 1
	end try
end writeWAVToFile

PS. If you’re going to be using Xcode and manipulating data, you may find ASObjC methods more convenient.
Don’t worry about line-wrapping in properly posted scripts. It’s not there when the scripts are opened with the “Open this Scriplet in your Editor:” link.

Hi Nigel…

Thanks a mill’…
Just goes to show how little I know… ;o)

There will be 8 files, (maybe 9 if I include an arbitrary file), and will all be 524332 bytes in size.

I have a shell method that will create all 512KB files in way less than a second and I WILL be changing the header section for frequency control thus all the files headers and data sections will need to be re-written per change. I have now put part header sections into sub-routines and changed the method of writing the data. I have not uploaded this yet.

So although I have uploaded another “Audio Function Generator Script.” as another upload today, I have already changed my code to suit the next upload for this file generation.

I will always get something working first then ask for less ugly or better methods.

What I am doing at the moment is proof of concept, I have no idea how good this script will be but my shell script AudioScope.sh on www.unix.com has evolved into a sophisticated text mode Audio Oscilloscope app’. I hope this is how this script will go, something totally different along with being a useful piece of kids’ level test gear.

Anyhow I have much more proffessional looking code that I can peruse over and for that I thank you…

It looks quick at file creation but I will have to see if I can adapt it to 8 files each 8 times the file length.

Now “rigged for silent running” to understand your code…

BFN.

EDIT:
I do have XCode on this machine but my iMac is virgin and this must work on a default virgin install, which is OSX 10.11.5 on the iMac.

Hi Nigel…

Hope you read this…

Well I tried your code and changed it to include six other files to be genberated, all at 524332 bytes in size.

Worked well, BUT, it took around 15 seconds to create all the files.

It looks as though I am going to have to use shell extensions as my version which I will upload on the other thread soon can easily do them in way less than a second.

As all of these files WILL need to be changed for alteration of frequency then the official AppleScript way is not practical…

Many thanks for your code I will certainly be stealing some parts of it in the future.

Keep an eye out for the next upload on the “Audio Function Generator Script.” thread soon.

Consider this thread closed, many thanks again.

Bazza…

Interesting! When I was testing my script yesterday, it was a little faster than yours.

But it seems mine’s affected by some sort of caching effect. If I open Script Editor and run your script, it takes about 3 seconds for the dialog to appear. If I then run mine, it only takes 2. However many times I run the scripts, yours takes about 3 seconds and mine about 2. (Timings as on my six-year-old MacBook Pro.)

But if I then quit Script Editor, reopen it, and run my script first, it’s 30 seconds before the dialog appears! Thereafter, it’s back to 2 with my script and 3 with yours. I can cut the run time for mine, when it goes first, to 23 seconds by commenting out the ‘starting at eof’, which isn’t really necessary here as the next writing position’s always the end of the file anyway.

I’ve never noticed this effect before ” presumably because I don’t normally have reason to do 16,386 file writes in one script run.

You’ll probably prefer the more consistent behaviour of your own script.

Both scripts can be speeded up by including enough data for, say, two cycles in each write and doing half as many writes.

Timings from Script Editor are dodgy unless the final intended use is in Script Editor; it adds all sorts of overhead because of all the events to be logged.

I ran your script in ASObjC Explorer and it takes around 0.4 seconds here. In Script Geek it’s about 0.2, and in an applet about 0.3.

Pick a number, any number…

Edit: I should add that the other method was much more variable, but generally in the 1-2 seconds range. So the “real world” difference is possibly more dramatic than you’re seeing.

Thanks, Shane. You’re up late, aren’t you? :o

Perhaps not of much practical use here, but for my own amusement, here’s a faster version which appends two cycles at a time and has a different approach to writing the data to the file. The Read/Write commands write just the header and one copy of the wave data to the file, ASObjC reads them back as NSData, sticks everything together, and writes the whole caboodle at once. It doesn’t suffer from the “first run” delay of my other script and the handler returns the data in mutable form, so they’re potentially manipulable.

use AppleScript version "2.4"
use scripting additions
use framework "Foundation"

set rootFolder to "/tmp/"
set headerData to «data rdat524946462400010057415645666D74201000000001000100401F0000401F0000010008006461746100000100»

set sinewaveData to «data rdat802600267FD9FFD9802600267FD9FFD9» -- 2 cycles' worth.
set sinewavePath to rootFolder & "sinewave.wav"
writeWAVToFile(POSIX file sinewavePath, headerData, sinewaveData, 4096)

set squarewaveData to «data rdatFFFFFFFF00000000FFFFFFFF00000000» -- Ditto.
set squarewavePath to rootFolder & "squarewave.wav"
writeWAVToFile(POSIX file squarewavePath, headerData, squarewaveData, 4096)

repeat
	set theChoice to button returned of (display dialog "Audio Function Generator, Version 0.00.01." buttons {"Sine", "Square", "Quit"} default button "Quit")
	if (theChoice is "Sine") then
		do shell script "/usr/bin/afplay " & quoted form of sinewavePath
	else if (theChoice is "Square") then
		do shell script "/usr/bin/afplay " & quoted form of squarewavePath
	else
		exit repeat
	end if
end repeat

on writeWAVToFile(ASFileOrAlias, headerData, waveData, numberOfCycles)
	set filePath to POSIX path of ASFileOrAlias
	set fRef to (open for access ASFileOrAlias with write permission)
	try
		-- Write the header to the file and read it back as NSMutableData.
		set eof fRef to 0
		write headerData to fRef
		set theMutableData to current application's class "NSMutableData"'s dataWithContentsOfFile:(filePath)
		-- Empty the file, write one round of wave data, read that back as NSData.
		set eof fRef to 0
		write waveData to fRef
		set waveNSData to current application's class "NSData"'s dataWithContentsOfFile:(filePath)
		-- Empty the file again for luck and close the write permission access.
		set eof fRef to 0
		close access fRef
	on error errMsg
		close access fRef
		display dialog errMsg buttons {"OK"} default button 1
	end try
	
	-- Append the required number of wave samples to the header data.
	repeat numberOfCycles times
		tell theMutableData to appendData:(waveNSData)
	end repeat
	-- Write the data to the file in one block.
	tell theMutableData to writeToFile:(filePath) atomically:true

	return theMutableData -- In case you want to manipulate it later.
end writeWAVToFile

Hi Nigel et al…

I don’t have AppleScript 2.4 on this machine as I am on my hols at the moment but will try it when I return home on my current iMac next week, in the meantime I will be using 2.2.1. I am assuming that that machine has version 2.4.

( BTW Nigel, OT, I live in the Midlands… ;o) )

Lastly @ Shane…

It seems that the script editor on this machine has little influence on calling the current shell portion of the script inside this:-

http://www.macscripter.net/viewtopic.php?id=45078

Remember I profess to know nothing about AppleScript-ing but I am always prepared to use age-old techniques to get a working version of anything I am trying to create, whether it is the correct way or not, ALWAYS initially as proof of concept before I carry on.

My phrase is “if there is a back door, find it” and as a complete amateur I have found always back door methods for my kids level projects.

I had seriously thought about an AudioScope in AppleScript but from what I have seen this is not possible with a default MacOS install.

I don’t want to use XCode just the tools that exist in a user default MacOS install.

The worst case scenario…

Anyhow it is good to know that I, as a complete amateur, have shown something to Nigel that he has never noticed. ;o)

Thanks guys, pity there is NOT a THANKS button to show appreciation of the time and effort you guys put in…

Bazza.

Not much more than usual, but I was trying to solve something I suspect you were also looking at, which is how to create NSData without writing to file. As usual, I solved it after I went to bed – which isn’t a great way to get a good night’s sleep :frowning:

Anyway, this should be faster – we’re getting down into the low milliseconds:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use framework "AppKit"
use scripting additions

set rootFolder to "/tmp/"
set headerData to «data rdat524946462400010057415645666D74201000000001000100401F0000401F0000010008006461746100000100»
set sinewaveData to «data rdat802600267FD9FFD9»
set sinewavePath to rootFolder & "sinewave.wav"
set squarewaveData to «data rdatFFFFFFFF00000000»
set squarewavePath to rootFolder & "squarewave.wav"
-- force conversion to NSAppleEventDescriptors and extract NSData
set anArray to current application's NSArray's arrayWithArray:{headerData, sinewaveData, squarewaveData}
set headerData to anArray's firstObject()'s |data|()'s mutableCopy()
set sinewaveData to (anArray's objectAtIndex:1)'s |data|()'s mutableCopy()
set squarewaveData to (anArray's objectAtIndex:2)'s |data|()'s mutableCopy()
repeat 13 times -- 8192 = 2^13
	sinewaveData's appendData:sinewaveData
	squarewaveData's appendData:squarewaveData
end repeat
set fullSinewaveData to (headerData's mutableCopy())
fullSinewaveData's appendData:sinewaveData
fullSinewaveData's writeToFile:sinewavePath atomically:true
set fullSquarewaveData to (headerData's mutableCopy())
fullSquarewaveData's appendData:squarewaveData
fullSquarewaveData's writeToFile:sinewavePath atomically:true

We can actually go a bit quicker by dispensing with afplay and thus never writing to disk at all, by deleting the two writeToFile:atomically: lines and appending:

set sineSound to current application's NSSound's alloc()'s initWithData:fullSinewaveData
set squareSound to current application's NSSound's alloc()'s initWithData:fullSquarewaveData
sineSound's play()
squareSound's play()

Right. But Script Editor is an editor, not a runner. The fact that Nigel’s version uses write commands that generate lots of Apple events means that Script Editor adds a lot of overhead interpreting them for its logging engine. It’s an artefact of the process Script Editor uses, and doesn’t happen in scripts run from non-editors.

Hi Shane…

Are these available on a virgin MacOS El-Capitan install for the applescript environment?
(I now know that Applescript Version 2.4 or later IS required.)

If XCode is required to get these extensions then this is a non-starter as my script has to work on this MBP OSX 10.7.5 which I do NOT intend to update to El-Capitan.

It has to be coded using the default tools available on a virgin install ONLY, NOT with XCode additions.

All my stuff I have coded over the years requires no extensions, unless otherwise specified.

AudiScope.sh required SOX as the main capture; but, I also included, /dev/urandom for ALL machines as a DEMO mode, SoundRecorder.exe for CygWin and CygWin64, arecord for ALSA Linux machines, Quicktime Player for this machine and finally /dev/dsp for OSS and Pulse Audio for Linux, which are default captures for all variations on theme.

So if the set statements above require XCode then this for me is a non-starter as I don’t intend to use major installs to get things working.

Again I will try it on my iMac if XCode is not required when I return home next week.

Now you see where I am coming from.

Both of you guys have done some work on my behalf and again where is the “THANKS” button to show appreciation?

Thank you both…

The code runs fine as-is under vanilla OSX 10.10 and later – so Yosemite, El Capitan or Sierra. It will also run under 10.9 if you change the version number to “2.3” and call it as script library, which is simple if a bit fiddly.

Well. If this were the Olympics, you’d be in with a chance for a gold. :cool:

I’d got as far as putting the data in a dictionary, but it would have taken me another year or two to stumble upon |data|(). Even now, I haven’t been able to find it in the documentation, apart from the NSData class method. (Edit: Just noticed the comment in the code: NSAppleEventDescriptor.)

Bazza:
The frameworks are part of Mac OS anyway and so are part of the standard installation, but their methods have only been directly accessible to AppleScript since Mac OS 10.9 Mavericks (AppleScript 2.3) and it’s only been possible to call the methods directly in running scripts since Mac OS 10.10 Yosemite (AppleScript 2.4). You don’t need Xcode to run the scripts, only to build applications with posh user interfaces and the the like. But once you’ve gone through the interminable download(s), it allows you to have the documentation for the methods conveniently to hand on your own computer. The extension to the language which accesses the methods is called AppleScriptObjectiveC, or ASObjC for short. It’s a cross between AppleScript and Objective-C and almost exactly unlike either of them. :wink:

Nigel,

Look up NSAppleEventDescriptor, which is the Cocoa equivalent of an AppleScript object. When you pass AS items to Cocoa, the ones that can be bridged are converted but the rest are stored as NSAppleEventDescriptors. Wrapping them in an array gives us a way of accessing them before they get converted back to AS objects.

Sneaky. :cool: But I suppose that only works until a future version of the bridge converts AS data too. :confused:

Given that Bazza may not find AS data objects particularly useful anyway, another approach would be to use hex strings and/or byte-value lists, convert to Base64, and feed that to NSData instead.

use AppleScript version "2.4"
use scripting additions
use framework "Foundation"

set rootFolder to "/tmp/"
set headerHex to "524946462400010057415645666D74201000000001000100401F0000401F0000010008006461746100000100"

set sinewaveHex to "802600267FD9FFD9802600267FD9FFD9" -- 2 cycles' worth.
set sinewavePath to rootFolder & "sinewave.wav"
writeWAVToFile(sinewavePath, headerHex, sinewaveHex, 4096)

set squarewaveHex to "FFFFFFFF00000000FFFFFFFF00000000" -- Ditto.
set squarewavePath to rootFolder & "squarewave.wav"
writeWAVToFile(squarewavePath, headerHex, squarewaveHex, 4096)

repeat
	set theChoice to button returned of (display dialog "Audio Function Generator, Version 0.00.01." buttons {"Sine", "Square", "Quit"} default button "Quit")
	if (theChoice is "Sine") then
		do shell script "/usr/bin/afplay " & quoted form of sinewavePath
	else if (theChoice is "Square") then
		do shell script "/usr/bin/afplay " & quoted form of squarewavePath
	else
		exit repeat
	end if
end repeat

on writeWAVToFile(posixPath, headerHex, waveHex, numberOfCycles)
	-- Convert the header hex string to byte values to Base64 to NSMutableData.
	set headerByteValues to hexToByteValues(headerHex)
	set headerBase64String to byteValuesToBase64(headerByteValues)
	set WAVData to current application's class "NSMutableData"'s alloc()'s initWithBase64EncodedString:(headerBase64String) options:(0)
	-- Ditto the wave hex string to NSData.
	set waveByteValues to hexToByteValues(waveHex)
	set waveBase64String to byteValuesToBase64(waveByteValues)
	set waveData to current application's class "NSData"'s alloc()'s initWithBase64EncodedString:(waveBase64String) options:(0)
	
	-- Append the required number of wave samples to the header data.
	repeat numberOfCycles times
		tell WAVData to appendData:(waveData)
	end repeat
	-- Write the data to the file.
	tell WAVData to writeToFile:(posixPath) atomically:true
	
	return WAVData -- In case you want to manipulate it later.
end writeWAVToFile

(* Given a string of an even number of hexadecimal digits, return a list of corresponding byte values. (Case insensitive.) *)
on hexToByteValues(hexString)
	script o
		property hexStringID : id of hexString
		property byteValues : {}
	end script
	
	set characterCount to (count hexString)
	if (characterCount mod 2 is 1) then error "hextToByteValues(): parameter string is odd length."
	-- The math is just something that happens to work with the character codes for hexadecimal digits.
	repeat with h from 1 to characterCount by 2
		tell ((item h of o's hexStringID) + 16) mod 32 to set hiVal to (it div 16 * 9 + it mod 16) * 16
		tell ((item (h + 1) of o's hexStringID) + 16) mod 32 to set end of o's byteValues to (it div 16 * 9 + it mod 16) + hiVal
	end repeat
	
	return o's byteValues
end hexToByteValues

(* Given a list of byte values, return the corresponding Base64 string. *)
on byteValuesToBase64(byteValues)
	script o
		property base64IDLookup : id of "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
		
		property paddedList : byteValues & {0, 0}
		property base64IDCollector : {}
	end script
	
	set byteCount to (count byteValues)
	set equalsID to id of "="
	repeat with i from 1 to byteCount by 3
		set {v1, v2, v3} to items i thru (i + 2) of o's paddedList
		set bitPattern to v1 * 65536 + v2 * 256 + v3
		set end of o's base64IDCollector to item (bitPattern div 262144 + 1) of o's base64IDLookup
		set end of o's base64IDCollector to item (bitPattern mod 262144 div 4096 + 1) of o's base64IDLookup
		if (i < byteCount) then
			set end of o's base64IDCollector to item (bitPattern mod 4096 div 64 + 1) of o's base64IDLookup
		else
			set end of o's base64IDCollector to equalsID
		end if
		if (i < byteCount - 1) then
			set end of o's base64IDCollector to item (bitPattern mod 64 + 1) of o's base64IDLookup
		else
			set end of o's base64IDCollector to equalsID
		end if
	end repeat
	
	return string id (o's base64IDCollector)
end byteValuesToBase64

OK guys, boy oh boy do I now feel inferior. ;oO
(Apologies for any typos…)
judging by the way you are both giving your code away I sense that I have hit a nerve and you are both interested. ;o)
(i will certainly read the pointers for much more info.)

@both of you…

The header will have to be split into three parts:-
“”"
header_start
header_rate
header_end
“”"
as…

By altering those hex values per frequency change the output frequency will correspond from 250Hz to 6000Hz just
with the data values given for all waveforms.
Note that two fixed header(_start)(_end) values are now set to give a data section of exactly 512KB.

I will thoroughly dissect your code when I get home next week when i can run it and deliberately crash it.

I really do appreciate what you guys have done and do so wish there was a THANKS button to acknpwledge the fact.

I have thoughts of amplitude modulation, sweep generation using a square wave only, arbitrary waveform generation and so on but I might just bite off more than I can chew.
Paraphrasing:- “Better to have tried and failed than to not try at all…”

Rigged for silent running for a while now.

I don’t think we need lose too much sleep over that possibility :slight_smile:

Or even:

set headerHex to "524946462400010057415645666D74201000000001000100401F0000401F0000010008006461746100000100"
set headerData to run script "«data rdat" & headerHex & "»"

Another option worth considering is using an AVAudioPlayer to play the sounds, rather than afplay or NSSound. That lets you change a few things – for example, you can make a sound loop. it needs a use framework “AVFoundation” and then:

set sineSound to current application's AVAudioPlayer's alloc()'s initWithData:fullSinewaveData |error|:(missing value)
sineSound's setNumberOfLoops:5
sineSound's play()

That way the data can be kept to an absolute minimum.

It also lets you fiddle with other settings, and synchronize sounds exactly.

Not in the project per se, but your original question of alternatives to fixed shell scripts full of "\x"s has inspired some interesting technical musings. :slight_smile: Hopefully they’ve given you a few ideas.

I did think of that, but didn’t offer it in view of my reservations about possible future versions of the scripting bridge and the potential for producing data objects that were too big for the compiler. However, I seem to have imagined the latter limit as I’m currently having no problem compiling data objects of enormous lengths ” even on my G5 running Leopard. Whatver happened to “Way too long, dude.”? :confused:

Depending on what Bazza ultimately wants to do, AVAudioPlayer sounds (!) ideal, although it doesn’t quite work in the way I’d expect. If I provide just one cycle of wave data and set the player to loop 8192 times, there’s just one solitary click. But 8192 cycles played once gives the full sound. 8192 cycles played twice gives the full sound for twice as long. My guess is that the player can’t loop fast enough for very short samples, so a practical compromise will need to be found.

use framework "Foundation"
-- use framework "appKit"
use framework "AVFoundation"
use scripting additions

set headerHex to "524946462400010057415645666D74201000000001000100401F0000401F0000010008006461746100000100"
set sinewaveHex to "802600267FD9FFD9"
-- repeat 13 times -- 2^13 = 8192
repeat 9 times -- 2^9 = 512. Sound gets choppy with a sample length of 2^8 (256) cycles.
	set sinewaveHex to sinewaveHex & sinewaveHex
end repeat
set fullSinewaveData to (run script "«data rdat" & headerHex & sinewaveHex & "»")
set fullSinewaveData to (current application's class "NSArray"'s arrayWithArray:({fullSinewaveData}))'s firstObject()'s |data|()

set sineSound to current application's class "AVAudioPlayer"'s alloc()'s initWithData:(fullSinewaveData) |error|:(missing value)
-- sineSound's setNumberOfLoops:(0) -- Play (parameter + 1) times.
sineSound's setNumberOfLoops:(15) -- Play (8192 / 512 = 16) times.
-- sineSound's prepareToPlay()
sineSound's play()

I suspect there’s some fiddling done to stop clicks when sounds start and end, and the duration of that fade probably takes quite a few cycles. It’s probably based on a minimum sound length.

Well guys with careful manipulation of the waveform generation one can overcome run time on-off clicks…

I have quickly written this in the shell scripting as a demo so apologies it is only proof of concept.

This can be applied to signed, unsigned, various bit depth integer, mono or stereo, audio files.

I have put it in quote tags rather than code tags…

When click2.wav is played you don’t hear anything…
The string can be "\x80…’ instead of ‘\x7F…’ it doesn’t really matter.

So long as you start a waveform with either of those values in 8 bit unsigned integer format the sound card is already sitting at ‘\x7F’ give or take a single bit error.

This is true for most players and sound systems in various platforms, including my old AMIGA A1200 when I was coding for that.

EDIT:
Added the ‘-d’ switch to make it easier to get a reference on screen when afplay starts.
(Watch for wordwrapping in “quote” mode.)