I know I'm quite late in reporting back on this - in truth I stalled very quickly when I realised that it was going to be tricky to actually extract something from the audio on the disc. The issue is that, through the various filters the loading noise has had applied to it, and then the subsequent audio compression when it was mastered onto the DVD, the edges of the signal have become very muddy. It's pretty much useless to try and load this directly into a Spectrum, it has to be cleaned up first.
Of course, the major problem with processing this signal is that the 0s and 1s are of different length in the Spectrum's tape encoding scheme - if you misidentify a bit, you end up shifting the entire stream after that point one bit left or right.
I had a go at writing a script to clean the audio yesterday: the general idea is that it uses a Schmitt trigger to more reliably detect the edges of the signal. This worked alright for periods of solid tone (i.e. runs of all 1s or 0s), but started coming apart when the 1s and 0s were placed together. I still got odd peaks of signal, and the input looked pretty thin - the duty cycle wasn't a proper 50%, but instead something like 20%.
In an attempt to ameliorate this, I wrote a second pass which would look for spikes in the output and make them the same amplitude as the surrounding parts. After this, there is a processing step in which periods of low and high signal are grouped together into cycles - the overall length of the cycles is then used to determine whether the bit is a 0 or a 1.
(Audacity waveforms -
top: the original signal,
middle: Schmitt trigger only,
bottom: Schmitt trigger and second pass)
However, this still isn't enough to get intelligible data. Some of the cycles are of an odd length, not quite 0 or 1, which means that there could be parts of the signal mistakenly being labelled as spikes upstream. Searching the TOSEC archive for short snippets of the raw data has given me no results so far. Knowing that the audio sounds like a screen, I've also tried loading the data into the Spectrum's display file to see if I can recognise any patterns in it - again, this has been a fruitless endeavour.
I will leave my Python code here in case anyone else wants to have a go at it - bear in mind that this was written "off the cuff" and isn't structured at all well.
Code: Select all
import wave
import numpy as np
def getWave(filename):
# taken from https://stackoverflow.com/a/62298670 (revision 21/06/2020)
# Read file to get buffer
ifile = wave.open(filename)
samples = ifile.getnframes()
audio = ifile.readframes(samples)
# Convert buffer to float32 using NumPy
audio_as_np_int16 = np.frombuffer(audio, dtype=np.int16)
audio_as_np_float32 = audio_as_np_int16.astype(np.float32)
# Normalise float32 array so that values are between -1.0 and +1.0
max_int16 = 2**15
audio_normalised = audio_as_np_float32 / max_int16
return audio_normalised
def maxMinPeaks(data, leftb, rightb):
lb = leftb
if (lb < 0):
lb = 0
rb = rightb
if (rb >= len(data)):
rb = len(data) - 1
return (data[lb:rb].max(), data[lb:rb].min())
def schmitt(cur, in2, ub, lb):
if (cur == -1):
if (in2 > ub):
return 1
else:
return -1
else:
if (in2 < lb):
return -1
else:
return 1
audioFreq = 48000
cpuFreq = 3.54 * 10**6 # The source sounds closer in pitch to the 128K models
tstate0 = 855
tstate1 = 1710
pulse0 = (tstate0 / cpuFreq) * audioFreq
pulse1 = (tstate1 / cpuFreq) * audioFreq
# Get time mid-way between 0 and 1 cycle
mid = ((pulse0 * 2) + (pulse1 * 2)) / 2
level = -1
waveData = getWave("ep00b.wav")
print(len(waveData))
maxData = np.full_like(waveData, 1)
minData = np.full_like(waveData, 1)
binData = []
waveOut = bytearray()
# Get max/min peaks within given window for the adaptive Schmitt trigger
for i in range(len(maxData)):
maxVal, minVal = maxMinPeaks(waveData, i - 40, i + 20)
maxData[i] = maxVal
minData[i] = minVal
run = 0
# Adaptive Schmitt trigger - process wave file into 1-bit run-length encoded file
for i in range(len(waveData)):
newLevel = schmitt(level, waveData[i], maxData[i]*0.1625, minData[i]*0.1325)
if (newLevel != level):
binData.append((level, run))
level = newLevel
run = 0
run = run + 1
# Now we do a second pass over the 1-bit quantised file, fixing up any little spikes here and there
curLRun = 0
curTRun = curLRun
curLev = -1
conData = []
print(len(binData))
# The idea - group together low and high pulse in one
for lev, run in binData:
if (run < 6):
curLRun = curLRun + run
continue
if (lev != curLev):
curLev = lev
if (lev == 1):
curTRun = curLRun
curLRun = 0
else:
conData.append(curTRun + curLRun)
curTRun = 0
curLRun = 0
curLRun = curLRun + run
bits = []
# Now, we write out a cleaned version of our wave for analysis in an audio editor
# (in 8-bit unsigned PCM)
# We also check the time-length of these runs and output a 0 or 1 bit accordingly
for run in conData:
cycle = run // 2
for i in range(cycle):
waveOut.extend(b'\x10')
for i in range(run - cycle):
waveOut.extend(b'\xf0')
if (run < mid):
bits.append(0)
else:
bits.append(1)
waveOutFile = open("out.pcm", mode="wb")
waveOutFile.write(bytes(waveOut))
waveOutFile.close()
# Now write out 8 shifted variations of the data to disk
for i in range(8):
rawData = bytearray()
rawDataFile = open("raw"+str(i)+".bin", mode="wb")
bit = int(2**i)
byte = 0
for b in bits:
if (b == 1):
byte = byte ^ bit
bit = bit // 2
if (bit < 1):
bit = 128
rawData.extend(bytes([byte]))
byte = 0
#print(rawData) # might be easier to copy data from the printed version if you're using grep
rawDataFile.write(bytes(rawData))
rawDataFile.close()